diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d52d27df155..5afad870362 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @google/blockly-eng \ No newline at end of file +* @RaspberryPiFoundation/blockly-collaborators diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 634b59bfad4..cc045f46aaf 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,46 +1,3 @@ # 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. - -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 -copyright to your changes, even after your contribution becomes part of our -codebase, so we need your permission to use and distribute your code. We also -need to be sure of various other things—for instance that you'll tell us if you -know that your code infringes on other people's patents. You don't have to sign -the CLA until after you've submitted your code for review and a member has -approved it, but you must do it before we can put your code into our codebase. - -### Larger changes - -Before you start working on a larger contribution, you should get in touch with -us first through the issue tracker with your idea so that we can help out and -possibly guide you. Coordinating up front makes it much easier to avoid -frustration later on. - -### Code reviews - -All submissions, including submissions by project members, require review. We -use Github pull requests for this purpose. - -### Browser compatibility - -We care strongly about making Blockly work on all browsers. As of 2022 we -support Edge, Chrome, Safari, and Firefox. We will not accept changes that only -work on a subset of those browsers. You can check [caniuse.com](https://caniuse.com/) -for compatibility information. - -### The small print - -Contributions made by corporations are covered by a different agreement than -the one above, the -[Software Grant and Corporate Contributor License Agreement](https://cla.developers.google.com/about/google-corporate). +Want to contribute? Great! Head over to the [Blockly Developers site](https://developers.google.com/blockly/guides/modify/contributing) for information on how to contribute. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index d346d87afe1..5d92329ee27 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -31,6 +31,15 @@ body: 1. 2. 3. + - type: textarea + id: priority + attributes: + label: Priority + description: Please help us understand the priority for this issue. The more information provided will help the team better assess urgency and complexity. + placeholder: | + Work effort: Is this a quick fix or will it require a significant amount of work? Is there a clear path to a solution or is further investigation required? + Impact: Which part of the service is impacted? How many users are experencing this issue? + Is there a known workaround for this issue? If so, please describe. If not, does it prevent a user from doing a core task? - type: textarea id: stack-trace attributes: diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 42f0d297aea..a1882a5298a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -20,6 +20,10 @@ updates: target-branch: 'develop' schedule: interval: 'weekly' + ignore: + # See notes in welcome_new_contributors.yml for details on this. + - dependency-name: 'actions/first-interaction' + versions: ['*'] commit-message: prefix: 'chore(deps)' labels: diff --git a/.github/workflows/appengine_deploy.yml b/.github/workflows/appengine_deploy.yml index 1dd6d2ffa92..851769f3d54 100644 --- a/.github/workflows/appengine_deploy.yml +++ b/.github/workflows/appengine_deploy.yml @@ -15,16 +15,17 @@ jobs: steps: # Checks-out the repository under $GITHUB_WORKSPACE. # When running manually this checks out the master branch. - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Prepare demo files # Install all dependencies, then copy all the files needed for demos. run: | + cd packages/blockly npm install npm run prepareDemos - name: Upload - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: appengine_files path: _deploy/ @@ -36,13 +37,13 @@ jobs: needs: prepare steps: - name: Download prepared files - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v6 with: name: appengine_files path: _deploy/ - name: Deploy to App Engine - uses: google-github-actions/deploy-appengine@v2.1.5 + uses: google-github-actions/deploy-appengine@v3.0.1 # For parameters see: # https://github.com/google-github-actions/deploy-appengine#inputs with: diff --git a/.github/workflows/assign_reviewers.yml b/.github/workflows/assign_reviewers.yml index 924bf5423a2..9382d1a570b 100644 --- a/.github/workflows/assign_reviewers.yml +++ b/.github/workflows/assign_reviewers.yml @@ -16,11 +16,10 @@ jobs: requested-reviewer: runs-on: ubuntu-latest permissions: - contents: read - issues: write + pull-requests: write steps: - name: Assign requested reviewer - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | try { diff --git a/.github/workflows/browser_test.yml b/.github/workflows/browser_test.yml index 3675af7b042..427a0963af7 100644 --- a/.github/workflows/browser_test.yml +++ b/.github/workflows/browser_test.yml @@ -5,13 +5,15 @@ name: Run browser manually on: workflow_dispatch: + schedule: + - cron: '0 6 * * 1' # Runs every Monday at 06:00 UTC permissions: contents: read jobs: build: - timeout-minutes: 10 + timeout-minutes: 120 runs-on: ${{ matrix.os }} strategy: @@ -23,8 +25,12 @@ jobs: # See supported Node.js release schedule at # https://nodejs.org/en/about/releases/ + defaults: + run: + working-directory: ./packages/blockly + steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false @@ -34,7 +40,7 @@ jobs: ssh://git@github.com/ - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: ${{ matrix.node-version }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c4ab688f8fd..f0cd08796e6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,12 +18,12 @@ jobs: # TODO (#2114): re-enable osx build. # os: [ubuntu-latest, macos-latest] os: [ubuntu-latest] - node-version: [18.x, 20.x, 22.x] + node-version: [18.x, 20.x, 22.x, 24.x] # See supported Node.js release schedule at # https://nodejs.org/en/about/releases/ steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false @@ -33,7 +33,7 @@ jobs: ssh://git@github.com/ - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: ${{ matrix.node-version }} @@ -43,6 +43,7 @@ jobs: - name: Linux Test Setup if: runner.os == 'Linux' run: source ./tests/scripts/setup_linux_env.sh + working-directory: ./packages/blockly - name: Run run: npm run test @@ -54,10 +55,10 @@ jobs: timeout-minutes: 5 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Use Node.js 20.x - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: 20.x @@ -71,10 +72,10 @@ jobs: timeout-minutes: 5 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Use Node.js 20.x - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: 20.x diff --git a/.github/workflows/conventional-label.yml b/.github/workflows/conventional-label.yml index 57eaaa6980c..69e5035f757 100644 --- a/.github/workflows/conventional-label.yml +++ b/.github/workflows/conventional-label.yml @@ -3,12 +3,34 @@ on: types: - opened - edited -name: conventional-release-labels +name: commit lint & label jobs: - label: + lint: runs-on: ubuntu-latest + env: + PR_TITLE: ${{ github.event.pull_request.title }} permissions: + pull-requests: read contents: read + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + - name: Setup node + uses: actions/setup-node@v5 + with: + node-version: lts/* + cache: npm + - name: Install dependencies + run: npm ci + - name: Check PR title + id: check-pr-title + run: echo "$PR_TITLE" | npx commitlint --verbose + + label: + runs-on: ubuntu-latest + permissions: + issues: write pull-requests: write steps: - uses: bcoe/conventional-release-labels@v1 diff --git a/.github/workflows/develop_freeze.yml b/.github/workflows/develop_freeze.yml deleted file mode 100644 index 395a34434dd..00000000000 --- a/.github/workflows/develop_freeze.yml +++ /dev/null @@ -1,26 +0,0 @@ -# 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/keyboard_plugin_test.yml b/.github/workflows/keyboard_plugin_test.yml new file mode 100644 index 00000000000..e64efe983c6 --- /dev/null +++ b/.github/workflows/keyboard_plugin_test.yml @@ -0,0 +1,66 @@ +# Workflow for running the keyboard navigation plugin's automated tests. + +name: Keyboard Navigation Automated Tests + +on: + workflow_dispatch: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + webdriverio_tests: + name: WebdriverIO tests + timeout-minutes: 10 + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + + steps: + - name: Checkout core Blockly + uses: actions/checkout@v5 + with: + path: core-blockly + + - name: Checkout keyboard navigation plugin + uses: actions/checkout@v5 + with: + repository: 'google/blockly-keyboard-experimentation' + ref: 'main' + path: blockly-keyboard-experimentation + + - name: Use Node.js 20.x + uses: actions/setup-node@v5 + with: + node-version: 20.x + + - name: NPM install + run: | + cd core-blockly + npm install + cd .. + cd blockly-keyboard-experimentation + npm install + cd .. + + - name: Link latest core main with plugin + run: | + cd core-blockly/packages/blockly + npm run package + cd dist + npm link + cd ../../../../blockly-keyboard-experimentation + npm link blockly + cd .. + + - name: Run keyboard navigation plugin tests + run: | + cd blockly-keyboard-experimentation + npm run test diff --git a/.github/workflows/tag_module_cleanup.yml b/.github/workflows/tag_module_cleanup.yml deleted file mode 100644 index d83d0e9371a..00000000000 --- a/.github/workflows/tag_module_cleanup.yml +++ /dev/null @@ -1,37 +0,0 @@ -# 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 index 37ca9ef89df..5ae294fdc5b 100644 --- a/.github/workflows/welcome_new_contributors.yml +++ b/.github/workflows/welcome_new_contributors.yml @@ -9,7 +9,12 @@ jobs: permissions: pull-requests: write steps: - - uses: actions/first-interaction@v1 + # NOTE TO DEVELOPER: Per #9447 this is pinned to v1.3.0 and all updates + # have been disabled for it. There are some largely incompatibilities on + # v2 and v3 of the action that, without resolution, will break the first + # interaction experience for new contributors. This dependency should not + # be upgraded until those issues are resolved. + - uses: actions/first-interaction@v1.3.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} pr-message: > @@ -20,10 +25,6 @@ jobs: 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). diff --git a/.gitignore b/.gitignore index 3c1938f17d9..e1b599dfef6 100644 --- a/.gitignore +++ b/.gitignore @@ -9,14 +9,3 @@ build-debug.log *.komodoproject /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/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index cbe1c7ee290..00000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,77 +0,0 @@ -# Changelog - -## [8.0.0](https://github.com/google/blockly/compare/blockly-v7.20211209.0...blockly-v8.0.0) (2022-03-31) - - -### ⚠ BREAKING CHANGES - -* change paste to return the pasted thing to support keyboard nav (#5996) -* **blocks:** ...and rename Blockly.blocks.all (blocks/all.js) to Blockly.libraryBlocks (blocks/blocks.js -* * refactor(blocks): Make loopTypes a Set -* allows previously internal constants to be configurable (#5897) -* * refactor(blocks): Make loopTypes a Set -* remove unused constants from internalConstants (#5889) - -### Features - -* add mocha failure messages to console output ([#5984](https://github.com/google/blockly/issues/5984)) ([7d250fa](https://github.com/google/blockly/commit/7d250fa9cfb30f95e7af523720b66c8b001df15c)) -* Allow developers to set a custom tooltip rendering function. ([#5956](https://github.com/google/blockly/issues/5956)) ([6841ccc](https://github.com/google/blockly/commit/6841ccc99fdbcc5f6d5a63bb36cb3b6ebd2be246)) -* **blocks:** Export block definitions ([#5908](https://github.com/google/blockly/issues/5908)) ([ffb8907](https://github.com/google/blockly/commit/ffb8907db8d0f11609c1fe14b2a450d3e639a871)) -* make mocha fail if it encounters 0 tests ([#5981](https://github.com/google/blockly/issues/5981)) ([0b2bf3a](https://github.com/google/blockly/commit/0b2bf3ae9d0c777f4d13d47692f5ae224dff1ec8)) -* **tests:** Add a test to validate `scripts/migration/renamings.js` ([#5980](https://github.com/google/blockly/issues/5980)) ([3c723f0](https://github.com/google/blockly/commit/3c723f0199b1f3b5eaac58f064b02d52b60d0ddb)) -* **tests:** Use official semver.org RegExp ([#5990](https://github.com/google/blockly/issues/5990)) ([afc4088](https://github.com/google/blockly/commit/afc4088ce278f97585f9ff5e65a921f7c4c65531)) - - -### Bug Fixes - -* Adds check for changedTouches ([#5869](https://github.com/google/blockly/issues/5869)) ([3f4f505](https://github.com/google/blockly/commit/3f4f5057919fdb4a329e9d2b15378c5c5831ae3b)) -* advanced playground and playground to work when hosted ([#6021](https://github.com/google/blockly/issues/6021)) ([364bf14](https://github.com/google/blockly/commit/364bf14ce6932f426591e3f53c1d066771ddcb8e)) -* always rename caller to legal name ([#6014](https://github.com/google/blockly/issues/6014)) ([c430800](https://github.com/google/blockly/commit/c4308007bc4b58d51adf1fda7b51ffa9f1d3f093)) -* **blocks:** correct the callType_ of procedures_defreturn ([#5974](https://github.com/google/blockly/issues/5974)) ([b34db5b](https://github.com/google/blockly/commit/b34db5bd01f7b532ebabc80264ca9fc804a76c75)) -* **build:** Correctly handle deep export paths in UMD wrapper ([#5945](https://github.com/google/blockly/issues/5945)) ([71ab146](https://github.com/google/blockly/commit/71ab146bc21aef9bdd6b2385c1df5f51e3ff5b58)) -* bumping a block after duplicate breaking undo ([#5844](https://github.com/google/blockly/issues/5844)) ([5204569](https://github.com/google/blockly/commit/5204569cff58c1ead7c15165a1351fa6a2ba2ad3)) -* change getCandidate_ and showInsertionMarker_ to be more dynamic ([#5722](https://github.com/google/blockly/issues/5722)) ([68d8113](https://github.com/google/blockly/commit/68d81132b851d20884ee9da41719fa62cdfce0ee)) -* change paste to return the pasted thing to support keyboard nav ([#5996](https://github.com/google/blockly/issues/5996)) ([20f1475](https://github.com/google/blockly/commit/20f1475afc1abf4b5e600219c2981150fc621ba5)) -* Change the truthy tests of width and height in WorkspaceSvg.setCachedParentSvgSize to actual comparisons with null so that zero value can be saved into the cache ([#5997](https://github.com/google/blockly/issues/5997)) ([fec44d9](https://github.com/google/blockly/commit/fec44d917e4b8475beba28e4769a50982425e887)) -* comments not being restored when dragging ([#6011](https://github.com/google/blockly/issues/6011)) ([85ce3b8](https://github.com/google/blockly/commit/85ce3b82c6c32e8a2a1608c6d604262ea0e5c38d)) -* convert the common renderer to an ES6 class ([#5978](https://github.com/google/blockly/issues/5978)) ([c1004be](https://github.com/google/blockly/commit/c1004be1f24debe1df1566e6067cf2f6769d51aa)) -* convert the Workspace class to an ES6 class ([#5977](https://github.com/google/blockly/issues/5977)) ([e2eaebe](https://github.com/google/blockly/commit/e2eaebec47b08a83eb36d0d04cefa254d1c5d666)) -* custom block context menus ([#5976](https://github.com/google/blockly/issues/5976)) ([8058df2](https://github.com/google/blockly/commit/8058df2a71dcecdc1190ae1d6f5dcccfafc980e8)) -* Don't throw if drag surface is empty. ([#5695](https://github.com/google/blockly/issues/5695)) ([769a25f](https://github.com/google/blockly/commit/769a25f4badffd2409ce19535344c98f5d8b01c9)) -* export Blockly.Names.NameType and Blockly.Input.Align correctly ([#6030](https://github.com/google/blockly/issues/6030)) ([2c15d00](https://github.com/google/blockly/commit/2c15d002ababcba7f34c526c05f231735e3e0169)) -* Export loopTypes from Blockly.blocks.loops ([#5900](https://github.com/google/blockly/issues/5900)) ([4f74210](https://github.com/google/blockly/commit/4f74210e74ef0b06216ab0f288268192674d69c9)) -* Export loopTypes from Blockly.blocks.loops ([#5900](https://github.com/google/blockly/issues/5900)) ([74ef1cb](https://github.com/google/blockly/commit/74ef1cbf521f7c6447ea9672ddbfe861d2292e5f)) -* Fix bug where workspace comments could not be created. ([#6024](https://github.com/google/blockly/issues/6024)) ([2cf8eb8](https://github.com/google/blockly/commit/2cf8eb87dcb029ba152b63b01ee7e4df431d1bb6)) -* Fix downloading screenshots on the playground. ([#6025](https://github.com/google/blockly/issues/6025)) ([ca6e590](https://github.com/google/blockly/commit/ca6e590101d511a8d98a5c5438af32ff6749e020)) -* fix keycodes type ([#5805](https://github.com/google/blockly/issues/5805)) ([0a96543](https://github.com/google/blockly/commit/0a96543a1179636e4efeb3c654c075952aca0c9f)) -* Fixed the label closure on demo/blockfactory ([#5833](https://github.com/google/blockly/issues/5833)) ([e8ea2e9](https://github.com/google/blockly/commit/e8ea2e9902fb9f642459e7341c3d59e19f359fca)) -* **generators:** Fix an operator precedence issue in the math_number_property generators to remove extra parentheses ([#5685](https://github.com/google/blockly/issues/5685)) ([a31003f](https://github.com/google/blockly/commit/a31003fab964e529152389029ec3126a3802851b)) -* incorrect module for event data in renamings database ([#6012](https://github.com/google/blockly/issues/6012)) ([e502eaa](https://github.com/google/blockly/commit/e502eaa6e1c88b2bb34e9a87917a15098b81cfa3)) -* Move [@alias](https://github.com/alias) onto classes instead of constructors ([#6003](https://github.com/google/blockly/issues/6003)) ([1647a32](https://github.com/google/blockly/commit/1647a3299ac48b5924f987015d8f3c47593922af)) -* move test helpers from samples into core ([#5969](https://github.com/google/blockly/issues/5969)) ([2edd228](https://github.com/google/blockly/commit/2edd22811752f05e16c68d593e5d1b809e24ed25)) -* move the dropdown div to a namespace instead of a class with only static properties ([#5979](https://github.com/google/blockly/issues/5979)) ([543cb8e](https://github.com/google/blockly/commit/543cb8e1b1c1a7fca5a1629f42f71c9b18e1a255)) -* msg imports in type definitions ([#5858](https://github.com/google/blockly/issues/5858)) ([07a75de](https://github.com/google/blockly/commit/07a75dee8de13b6c5a02959325a0155d413d6712)) -* opening/closing the mutators ([#6000](https://github.com/google/blockly/issues/6000)) ([243fc52](https://github.com/google/blockly/commit/243fc52a96e1089aad89ff6b642c6605d8f71afd)) -* playground access to Blockly ([9e1cda8](https://github.com/google/blockly/commit/9e1cda8f45cea1707c5a228d5ce79b4cd81566f8)) -* playground test blocks, text area listeners, and show/hide buttons ([#6015](https://github.com/google/blockly/issues/6015)) ([7abf3de](https://github.com/google/blockly/commit/7abf3de910a35e1a6086a3243570627a41e73339)) -* procedure param edits breaking undo ([#5845](https://github.com/google/blockly/issues/5845)) ([8a71f87](https://github.com/google/blockly/commit/8a71f879504503f4aec1140fe653d93602c664df)) -* re-expose HSV_VALUE and HSV_SATURATION as settable properties on Blockly ([#5821](https://github.com/google/blockly/issues/5821)) ([0e5f3ce](https://github.com/google/blockly/commit/0e5f3ce6074fbbb2923e9a62bffefeae0a813be8)) -* re-expose HSV_VALUE and HSV_SATURATION as settable properties on Blockly ([#5821](https://github.com/google/blockly/issues/5821)) ([6fc3316](https://github.com/google/blockly/commit/6fc3316309534270106050f0e1fecb7a09b8e62c)) -* revert "Delete events should animate when played ([#5919](https://github.com/google/blockly/issues/5919))" ([#6031](https://github.com/google/blockly/issues/6031)) ([c4a25eb](https://github.com/google/blockly/commit/c4a25eb3c432b0e8a9a18aae42839d163a177c48)) -* revert converting test helpers to es modules ([#5982](https://github.com/google/blockly/issues/5982)) ([01d4597](https://github.com/google/blockly/commit/01d45972d4df8b5e4afa4a19d93defb8961fea57)) -* setting null for a font style on a theme ([#5831](https://github.com/google/blockly/issues/5831)) ([835fb02](https://github.com/google/blockly/commit/835fb02343df0a4b9dab7704a4b3d8be8e9a497c)) -* **tests:** Enable --debug for test:compile:advanced; fix some errors ([#5959](https://github.com/google/blockly/issues/5959)) ([88334be](https://github.com/google/blockly/commit/88334bea80aa26c08705f16aba5e79dd708158f9)) -* **tests:** Enable `--debug` for `test:compile:advanced`; fix some errors (and demote the rest to warnings) ([#5983](https://github.com/google/blockly/issues/5983)) ([e11b583](https://github.com/google/blockly/commit/e11b5834e5e4e8fe991be32afb08eafa7c8adffd)) -* TypeScript exporting of the serialization functions ([#5890](https://github.com/google/blockly/issues/5890)) ([5d7c890](https://github.com/google/blockly/commit/5d7c890243ba7d0501514ba48778715097ce5a3b)) -* undo/redo for auto disabling if-return blocks ([#6018](https://github.com/google/blockly/issues/6018)) ([c7a359a](https://github.com/google/blockly/commit/c7a359a8424287f139752573a27a8a6eb97cb7b3)) -* update the playground to load compressed when hosted ([#5835](https://github.com/google/blockly/issues/5835)) ([2adf326](https://github.com/google/blockly/commit/2adf326c230589800880faa9599eca2ecc94d283)) -* Update typings for q1 2022 release ([#6051](https://github.com/google/blockly/issues/6051)) ([69f3d4a](https://github.com/google/blockly/commit/69f3d4ae89ce16a558443dd0a772e35b62c096d3)) -* Use correct namespace for svgMath functions ([#5813](https://github.com/google/blockly/issues/5813)) ([b8cc983](https://github.com/google/blockly/commit/b8cc983324338b2cbd536425c93ff3e7d512751e)) -* Use correct namespace for svgMath functions ([#5813](https://github.com/google/blockly/issues/5813)) ([025bab6](https://github.com/google/blockly/commit/025bab656669f99ebdb8b95bea39ebae296f1495)) - - -### Code Refactoring - -* allows previously internal constants to be configurable ([#5897](https://github.com/google/blockly/issues/5897)) ([4b5733e](https://github.com/google/blockly/commit/4b5733e7c85f2e196719550a3cfdcbcbd61739df)) -* **blocks:** Rename Blockly.blocks.* modules to Blockly.libraryBlocks.* ([#5953](https://github.com/google/blockly/issues/5953)) ([5078dcb](https://github.com/google/blockly/commit/5078dcbc6d4d48422313732e87191b29569b5eed)) -* remove unused constants from internalConstants ([#5889](https://github.com/google/blockly/issues/5889)) ([f0b1077](https://github.com/google/blockly/commit/f0b10776eb0657a5446adcfc62ad13f419c14271)) diff --git a/README.md b/README.md index 5a0f3b8f27b..65cde66c6ec 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,23 @@ # 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. +Blockly is a library that adds a visual code editor to web and mobile apps. The Blockly editor uses interlocking, graphical blocks to represent code concepts like variables, logical expressions, loops, and more. It allows users to apply programming principles without having to worry about syntax or the intimidation of a blinking cursor on the command line. All code is [free and open source](https://github.com/raspberrypifoundation/blockly/blob/develop/LICENSE). -![](https://developers.google.com/blockly/images/sample.png) +![Sample](./sample.svg) ## Getting Started with Blockly -Blockly has many resources for learning how to use the library. Start at our [Google Developers Site](https://developers.google.com/blockly) to read the documentation on how to get started, configure Blockly, and integrate it into your application. The developers site also contains links to: +Blockly has many resources for learning how to use the library. Start at our [Developers Site](https://developers.google.com/blockly) to read the documentation on how to get started, configure Blockly, and integrate it into your application. The developers site also contains links to: - [Getting Started article](https://developers.google.com/blockly/guides/get-started/web) - [Getting Started codelab](https://blocklycodelabs.dev/codelabs/getting-started/index.html#0) - [More codelabs](https://blocklycodelabs.dev/) -- [Demos and plugins](https://google.github.io/blockly-samples/) +- [Demos and plugins](https://raspberrypifoundation.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. +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. ### Installing Blockly -Blockly is [available on npm](https://www.npmjs.com/package/blockly). +Blockly is [available on npm](https://www.npmjs.com/package/blockly): ```bash npm install blockly @@ -34,15 +32,15 @@ For more information on installing and using Blockly, see the [Getting Started a ### 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). +We have a number of resources such as [examples](https://github.com/raspberrypifoundation/blockly-samples/tree/main/examples), [codelabs](https://github.com/raspberrypifoundation/blockly-samples/tree/main/codelabs), and [plugins](https://github.com/raspberrypifoundation/blockly-samples/tree/main/plugins) in another repository called [blockly-samples](https://github.com/raspberrypifoundation/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/programming/plugin_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. +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/raspberrypifoundation/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 release new versions on npm and GitHub releases, and then update our [docs](https://developers.google.com/blockly) and [demo pages](https://raspberrypifoundation.github.io/blockly-samples/). If there are breaking regressions, such as a crash when performing a standard action or a rendering issue that makes Blockly unusable, we will cherry-pick fixes into patch releases. The [releases page](https://github.com/raspberrypifoundation/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. @@ -56,19 +54,15 @@ As it is a beta channel, it may be less stable, and the APIs there are subject t ### Branches -There are two main branches for Blockly. +Most development happens in the **[main](https://github.com/raspberrypifoundation/blockly/tree/main)** branch. Pull requests should typically be made against main. This branch should be stable; features that aren't ready yet should be merged to a feature branch instead. Once something is in main we expect it to be part of the next release. However, features and APIs here are subject to change until they are released. If you're working on a production application using Blockly, you should use the release from npm or the GitHub release page, not the `main` branch. -**[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. +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. They will be merged into main when ready. ### 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. +Once a new API is released, it is considered beta until the following release. We generally try to avoid changing an API after it has been released, 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. +Unreleased APIs may change radically. Anything that is in `main` but not released is subject to change without warning. ## Issues and Milestones diff --git a/commitlint.config.mjs b/commitlint.config.mjs new file mode 100644 index 00000000000..f944970267c --- /dev/null +++ b/commitlint.config.mjs @@ -0,0 +1,39 @@ +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Rules configuration for commitlint. + * https://commitlint.js.org/reference/rules.html#subject-full-stop + * + * Extends the conventional-commit spec at + * https://github.com/conventional-changelog/commitlint/tree/master/@commitlint/config-conventional + */ + +export default { + extends: ['@commitlint/config-conventional'], + rules: { + // Warn if not in this list. Allow for judicious creativity. + 'type-enum': [ + 1, + 'always', + [ + 'build', + 'chore', + 'ci', + 'docs', + 'feat', + 'fix', + 'refactor', + 'release', + 'revert', + 'test', + ], + ], + 'subject-case': [0], + }, + helpUrl: + 'https://developers.google.com/blockly/guides/contribute/get-started/commits', +}; diff --git a/core/clipboard.ts b/core/clipboard.ts deleted file mode 100644 index ba6f44e6f4c..00000000000 --- a/core/clipboard.ts +++ /dev/null @@ -1,115 +0,0 @@ -/** - * @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.isMutator - ? 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/dialog.ts b/core/dialog.ts deleted file mode 100644 index 7e21129855c..00000000000 --- a/core/dialog.ts +++ /dev/null @@ -1,121 +0,0 @@ -/** - * @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/events/events_marker_move.ts b/core/events/events_marker_move.ts deleted file mode 100644 index 58309df5896..00000000000 --- a/core/events/events_marker_move.ts +++ /dev/null @@ -1,133 +0,0 @@ -/** - * @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/insertion_marker_manager.ts b/core/insertion_marker_manager.ts deleted file mode 100644 index 13d63042002..00000000000 --- a/core/insertion_marker_manager.ts +++ /dev/null @@ -1,742 +0,0 @@ -/** - * @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/interfaces/i_ast_node_location.ts b/core/interfaces/i_ast_node_location.ts deleted file mode 100644 index cc90bbc4065..00000000000 --- a/core/interfaces/i_ast_node_location.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @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 deleted file mode 100644 index 729e5f09543..00000000000 --- a/core/interfaces/i_ast_node_location_svg.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * @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 deleted file mode 100644 index b04234fd4a8..00000000000 --- a/core/interfaces/i_ast_node_location_with_block.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * @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_has_bubble.ts b/core/interfaces/i_has_bubble.ts deleted file mode 100644 index 276feff21e2..00000000000 --- a/core/interfaces/i_has_bubble.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * @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_selectable.ts b/core/interfaces/i_selectable.ts deleted file mode 100644 index 972b0adb107..00000000000 --- a/core/interfaces/i_selectable.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * @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/keyboard_nav/ast_node.ts b/core/keyboard_nav/ast_node.ts deleted file mode 100644 index 3b0efae3fb3..00000000000 --- a/core/keyboard_nav/ast_node.ts +++ /dev/null @@ -1,880 +0,0 @@ -/** - * @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 deleted file mode 100644 index 7526141529e..00000000000 --- a/core/keyboard_nav/basic_cursor.ts +++ /dev/null @@ -1,222 +0,0 @@ -/** - * @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 deleted file mode 100644 index 92279da562d..00000000000 --- a/core/keyboard_nav/cursor.ts +++ /dev/null @@ -1,137 +0,0 @@ -/** - * @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 deleted file mode 100644 index e3b438e6efe..00000000000 --- a/core/keyboard_nav/marker.ts +++ /dev/null @@ -1,111 +0,0 @@ -/** - * @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 deleted file mode 100644 index 0392887a1fd..00000000000 --- a/core/keyboard_nav/tab_navigate_cursor.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * @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/renderers/common/marker_svg.ts b/core/renderers/common/marker_svg.ts deleted file mode 100644 index 057324f0346..00000000000 --- a/core/renderers/common/marker_svg.ts +++ /dev/null @@ -1,767 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -// Former goog.module ID: Blockly.blockRendering.MarkerSvg - -// Unused import preserved for side-effects. Remove if unneeded. -import '../../events/events_marker_move.js'; - -import type {BlockSvg} from '../../block_svg.js'; -import type {Connection} from '../../connection.js'; -import {ConnectionType} from '../../connection_type.js'; -import {EventType} from '../../events/type.js'; -import * as eventUtils from '../../events/utils.js'; -import type {Field} from '../../field.js'; -import {FlyoutButton} from '../../flyout_button.js'; -import type {IASTNodeLocationSvg} from '../../interfaces/i_ast_node_location_svg.js'; -import {ASTNode} from '../../keyboard_nav/ast_node.js'; -import type {Marker} from '../../keyboard_nav/marker.js'; -import type {RenderedConnection} from '../../rendered_connection.js'; -import * as dom from '../../utils/dom.js'; -import {Svg} from '../../utils/svg.js'; -import * as svgPaths from '../../utils/svg_paths.js'; -import type {WorkspaceSvg} from '../../workspace_svg.js'; -import type {ConstantProvider, Notch, PuzzleTab} from './constants.js'; - -/** The name of the CSS class for a cursor. */ -const CURSOR_CLASS = 'blocklyCursor'; - -/** The name of the CSS class for a marker. */ -const MARKER_CLASS = 'blocklyMarker'; - -/** - * What we multiply the height by to get the height of the marker. - * Only used for the block and block connections. - */ -const HEIGHT_MULTIPLIER = 3 / 4; - -/** - * Class for a marker, containing methods for graphically rendering a marker as - * SVG. - */ -export class MarkerSvg { - /** - * The workspace, field, or block that the marker SVG element should be - * attached to. - */ - protected parent: IASTNodeLocationSvg | null = null; - - /** The current SVG element for the marker. */ - currentMarkerSvg: SVGElement | null = null; - colour_: string; - - /** The root SVG group containing the marker. */ - protected markerSvg_: SVGGElement | null = null; - protected svgGroup_: SVGGElement | null = null; - - protected markerBlock_: SVGPathElement | null = null; - - protected markerInput_: SVGPathElement | null = null; - protected markerSvgLine_: SVGRectElement | null = null; - - protected markerSvgRect_: SVGRectElement | null = null; - - /** The constants necessary to draw the marker. */ - protected constants_: ConstantProvider; - - /** - * @param workspace The workspace the marker belongs to. - * @param constants The constants for the renderer. - * @param marker The marker to draw. - */ - constructor( - protected readonly workspace: WorkspaceSvg, - constants: ConstantProvider, - protected readonly marker: Marker, - ) { - this.constants_ = constants; - - const defaultColour = this.isCursor() - ? this.constants_.CURSOR_COLOUR - : this.constants_.MARKER_COLOUR; - - /** The colour of the marker. */ - this.colour_ = marker.colour || defaultColour; - } - - /** - * Return the root node of the SVG or null if none exists. - * - * @returns The root SVG node. - */ - getSvgRoot(): SVGElement | null { - return this.svgGroup_; - } - - /** - * Get the marker. - * - * @returns The marker to draw for. - */ - getMarker(): Marker { - return this.marker; - } - - /** - * True if the marker should be drawn as a cursor, false otherwise. - * A cursor is drawn as a flashing line. A marker is drawn as a solid line. - * - * @returns True if the marker is a cursor, false otherwise. - */ - isCursor(): boolean { - return this.marker.type === 'cursor'; - } - - /** - * Create the DOM element for the marker. - * - * @returns The marker controls SVG group. - */ - createDom(): SVGElement { - const className = this.isCursor() ? CURSOR_CLASS : MARKER_CLASS; - - this.svgGroup_ = dom.createSvgElement(Svg.G, {'class': className}); - - this.createDomInternal_(); - return this.svgGroup_; - } - - /** - * Attaches the SVG root of the marker to the SVG group of the parent. - * - * @param newParent The workspace, field, or block that the marker SVG element - * should be attached to. - */ - protected setParent_(newParent: IASTNodeLocationSvg) { - if (!this.isCursor()) { - if (this.parent) { - this.parent.setMarkerSvg(null); - } - newParent.setMarkerSvg(this.getSvgRoot()); - } else { - if (this.parent) { - this.parent.setCursorSvg(null); - } - newParent.setCursorSvg(this.getSvgRoot()); - } - this.parent = newParent; - } - - /** - * Update the marker. - * - * @param oldNode The previous node the marker was on or null. - * @param curNode The node that we want to draw the marker for. - */ - draw(oldNode: ASTNode, curNode: ASTNode) { - if (!curNode) { - this.hide(); - return; - } - - this.constants_ = this.workspace.getRenderer().getConstants(); - - const defaultColour = this.isCursor() - ? this.constants_.CURSOR_COLOUR - : this.constants_.MARKER_COLOUR; - this.colour_ = this.marker.colour || defaultColour; - this.applyColour_(curNode); - - this.showAtLocation_(curNode); - - this.fireMarkerEvent(oldNode, curNode); - - // Ensures the marker will be visible immediately after the move. - const animate = this.currentMarkerSvg!.childNodes[0]; - if ( - animate !== undefined && - (animate as SVGAnimationElement).beginElement - ) { - (animate as SVGAnimationElement).beginElement(); - } - } - - /** - * Update the marker's visible state based on the type of curNode.. - * - * @param curNode The node that we want to draw the marker for. - */ - protected showAtLocation_(curNode: ASTNode) { - const curNodeAsConnection = curNode.getLocation() as Connection; - const connectionType = curNodeAsConnection.type; - if (curNode.getType() === ASTNode.types.BLOCK) { - this.showWithBlock_(curNode); - } else if (curNode.getType() === ASTNode.types.OUTPUT) { - this.showWithOutput_(curNode); - } else if (connectionType === ConnectionType.INPUT_VALUE) { - this.showWithInput_(curNode); - } else if (connectionType === ConnectionType.NEXT_STATEMENT) { - this.showWithNext_(curNode); - } else if (curNode.getType() === ASTNode.types.PREVIOUS) { - this.showWithPrevious_(curNode); - } else if (curNode.getType() === ASTNode.types.FIELD) { - this.showWithField_(curNode); - } else if (curNode.getType() === ASTNode.types.WORKSPACE) { - this.showWithCoordinates_(curNode); - } else if (curNode.getType() === ASTNode.types.STACK) { - this.showWithStack_(curNode); - } else if (curNode.getType() === ASTNode.types.BUTTON) { - this.showWithButton_(curNode); - } - } - - /************************** - * Display - **************************/ - - /** - * Show the marker as a combination of the previous connection and block, - * the output connection and block, or just the block. - * - * @param curNode The node to draw the marker for. - */ - protected showWithBlockPrevOutput(curNode: ASTNode) { - const block = curNode.getSourceBlock() as BlockSvg; - const width = block.width; - const height = block.height; - const markerHeight = height * HEIGHT_MULTIPLIER; - const markerOffset = this.constants_.CURSOR_BLOCK_PADDING; - - if (block.previousConnection) { - const connectionShape = this.constants_.shapeFor( - block.previousConnection, - ) as Notch; - this.positionPrevious_( - width, - markerOffset, - markerHeight, - connectionShape, - ); - } else if (block.outputConnection) { - const connectionShape = this.constants_.shapeFor( - block.outputConnection, - ) as PuzzleTab; - this.positionOutput_(width, height, connectionShape); - } else { - this.positionBlock_(width, markerOffset, markerHeight); - } - this.setParent_(block); - this.showCurrent_(); - } - - /** - * Position and display the marker for a block. - * - * @param curNode The node to draw the marker for. - */ - protected showWithBlock_(curNode: ASTNode) { - this.showWithBlockPrevOutput(curNode); - } - - /** - * Position and display the marker for a previous connection. - * - * @param curNode The node to draw the marker for. - */ - protected showWithPrevious_(curNode: ASTNode) { - this.showWithBlockPrevOutput(curNode); - } - - /** - * Position and display the marker for an output connection. - * - * @param curNode The node to draw the marker for. - */ - protected showWithOutput_(curNode: ASTNode) { - this.showWithBlockPrevOutput(curNode); - } - - /** - * Position and display the marker for a workspace coordinate. - * This is a horizontal line. - * - * @param curNode The node to draw the marker for. - */ - protected showWithCoordinates_(curNode: ASTNode) { - const wsCoordinate = curNode.getWsCoordinate(); - let x = wsCoordinate.x; - const y = wsCoordinate.y; - - if (this.workspace.RTL) { - x -= this.constants_.CURSOR_WS_WIDTH; - } - - this.positionLine_(x, y, this.constants_.CURSOR_WS_WIDTH); - this.setParent_(this.workspace); - this.showCurrent_(); - } - - /** - * Position and display the marker for a field. - * This is a box around the field. - * - * @param curNode The node to draw the marker for. - */ - protected showWithField_(curNode: ASTNode) { - const field = curNode.getLocation() as Field; - const width = field.getSize().width; - const height = field.getSize().height; - - this.positionRect_(0, 0, width, height); - this.setParent_(field); - this.showCurrent_(); - } - - /** - * Position and display the marker for an input. - * This is a puzzle piece. - * - * @param curNode The node to draw the marker for. - */ - protected showWithInput_(curNode: ASTNode) { - const connection = curNode.getLocation() as RenderedConnection; - const sourceBlock = connection.getSourceBlock(); - - this.positionInput_(connection); - this.setParent_(sourceBlock); - this.showCurrent_(); - } - - /** - * Position and display the marker for a next connection. - * This is a horizontal line. - * - * @param curNode The node to draw the marker for. - */ - protected showWithNext_(curNode: ASTNode) { - const connection = curNode.getLocation() as RenderedConnection; - const targetBlock = connection.getSourceBlock(); - let x = 0; - const y = connection.getOffsetInBlock().y; - const width = targetBlock.getHeightWidth().width; - if (this.workspace.RTL) { - x = -width; - } - this.positionLine_(x, y, width); - this.setParent_(targetBlock); - this.showCurrent_(); - } - - /** - * Position and display the marker for a stack. - * This is a box with extra padding around the entire stack of blocks. - * - * @param curNode The node to draw the marker for. - */ - protected showWithStack_(curNode: ASTNode) { - const block = curNode.getLocation() as BlockSvg; - - // Gets the height and width of entire stack. - const heightWidth = block.getHeightWidth(); - - // Add padding so that being on a stack looks different than being on a - // block. - const width = heightWidth.width + this.constants_.CURSOR_STACK_PADDING; - const height = heightWidth.height + this.constants_.CURSOR_STACK_PADDING; - - // Shift the rectangle slightly to upper left so padding is equal on all - // sides. - const xPadding = -this.constants_.CURSOR_STACK_PADDING / 2; - const yPadding = -this.constants_.CURSOR_STACK_PADDING / 2; - - let x = xPadding; - const y = yPadding; - - if (this.workspace.RTL) { - x = -(width + xPadding); - } - this.positionRect_(x, y, width, height); - this.setParent_(block); - this.showCurrent_(); - } - - /** - * Position and display the marker for a flyout button. - * This is a box with extra padding around the button. - * - * @param curNode The node to draw the marker for. - */ - protected showWithButton_(curNode: ASTNode) { - const button = curNode.getLocation() as FlyoutButton; - - // Gets the height and width of entire stack. - const heightWidth = {height: button.height, width: button.width}; - - // Add padding so that being on a button looks similar to being on a stack. - const width = heightWidth.width + this.constants_.CURSOR_STACK_PADDING; - const height = heightWidth.height + this.constants_.CURSOR_STACK_PADDING; - - // Shift the rectangle slightly to upper left so padding is equal on all - // sides. - const xPadding = -this.constants_.CURSOR_STACK_PADDING / 2; - const yPadding = -this.constants_.CURSOR_STACK_PADDING / 2; - - let x = xPadding; - const y = yPadding; - - if (this.workspace.RTL) { - x = -(width + xPadding); - } - this.positionRect_(x, y, width, height); - this.setParent_(button); - this.showCurrent_(); - } - - /** Show the current marker. */ - protected showCurrent_() { - this.hide(); - if (this.currentMarkerSvg) { - this.currentMarkerSvg.style.display = ''; - } - } - - /************************** - * Position - **************************/ - - /** - * Position the marker for a block. - * Displays an outline of the top half of a rectangle around a block. - * - * @param width The width of the block. - * @param markerOffset The extra padding for around the block. - * @param markerHeight The height of the marker. - */ - protected positionBlock_( - width: number, - markerOffset: number, - markerHeight: number, - ) { - const markerPath = - svgPaths.moveBy(-markerOffset, markerHeight) + - svgPaths.lineOnAxis('V', -markerOffset) + - svgPaths.lineOnAxis('H', width + markerOffset * 2) + - svgPaths.lineOnAxis('V', markerHeight); - if (!this.markerBlock_) { - throw new Error( - 'createDom should be called before positioning the marker', - ); - } - this.markerBlock_.setAttribute('d', markerPath); - if (this.workspace.RTL) { - this.flipRtl(this.markerBlock_); - } - this.currentMarkerSvg = this.markerBlock_; - } - - /** - * Position the marker for an input connection. - * Displays a filled in puzzle piece. - * - * @param connection The connection to position marker around. - */ - protected positionInput_(connection: RenderedConnection) { - const x = connection.getOffsetInBlock().x; - const y = connection.getOffsetInBlock().y; - - const path = - svgPaths.moveTo(0, 0) + - (this.constants_.shapeFor(connection) as PuzzleTab).pathDown; - - this.markerInput_!.setAttribute('d', path); - this.markerInput_!.setAttribute( - 'transform', - 'translate(' + - x + - ',' + - y + - ')' + - (this.workspace.RTL ? ' scale(-1 1)' : ''), - ); - this.currentMarkerSvg = this.markerInput_; - } - - /** - * Move and show the marker at the specified coordinate in workspace units. - * Displays a horizontal line. - * - * @param x The new x, in workspace units. - * @param y The new y, in workspace units. - * @param width The new width, in workspace units. - */ - protected positionLine_(x: number, y: number, width: number) { - if (!this.markerSvgLine_) { - throw new Error('createDom should be called before positioning the line'); - } - this.markerSvgLine_.setAttribute('x', `${x}`); - this.markerSvgLine_.setAttribute('y', `${y}`); - this.markerSvgLine_.setAttribute('width', `${width}`); - this.currentMarkerSvg = this.markerSvgLine_; - } - - /** - * Position the marker for an output connection. - * Displays a puzzle outline and the top and bottom path. - * - * @param width The width of the block. - * @param height The height of the block. - * @param connectionShape The shape object for the connection. - */ - protected positionOutput_( - width: number, - height: number, - connectionShape: PuzzleTab, - ) { - if (!this.markerBlock_) { - throw new Error( - 'createDom should be called before positioning the output', - ); - } - const markerPath = - svgPaths.moveBy(width, 0) + - svgPaths.lineOnAxis('h', -(width - connectionShape.width)) + - svgPaths.lineOnAxis('v', this.constants_.TAB_OFFSET_FROM_TOP) + - connectionShape.pathDown + - svgPaths.lineOnAxis('V', height) + - svgPaths.lineOnAxis('H', width); - this.markerBlock_.setAttribute('d', markerPath); - if (this.workspace.RTL) { - this.flipRtl(this.markerBlock_); - } - this.currentMarkerSvg = this.markerBlock_; - } - - /** - * Position the marker for a previous connection. - * Displays a half rectangle with a notch in the top to represent the previous - * connection. - * - * @param width The width of the block. - * @param markerOffset The offset of the marker from around the block. - * @param markerHeight The height of the marker. - * @param connectionShape The shape object for the connection. - */ - protected positionPrevious_( - width: number, - markerOffset: number, - markerHeight: number, - connectionShape: Notch, - ) { - if (!this.markerBlock_) { - throw new Error( - 'createDom should be called before positioning the previous connection marker', - ); - } - const markerPath = - svgPaths.moveBy(-markerOffset, markerHeight) + - svgPaths.lineOnAxis('V', -markerOffset) + - svgPaths.lineOnAxis('H', this.constants_.NOTCH_OFFSET_LEFT) + - connectionShape.pathLeft + - svgPaths.lineOnAxis('H', width + markerOffset * 2) + - svgPaths.lineOnAxis('V', markerHeight); - this.markerBlock_.setAttribute('d', markerPath); - if (this.workspace.RTL) { - this.flipRtl(this.markerBlock_); - } - this.currentMarkerSvg = this.markerBlock_; - } - - /** - * Move and show the marker at the specified coordinate in workspace units. - * Displays a filled in rectangle. - * - * @param x The new x, in workspace units. - * @param y The new y, in workspace units. - * @param width The new width, in workspace units. - * @param height The new height, in workspace units. - */ - protected positionRect_(x: number, y: number, width: number, height: number) { - if (!this.markerSvgRect_) { - throw new Error('createDom should be called before positioning the rect'); - } - this.markerSvgRect_.setAttribute('x', `${x}`); - this.markerSvgRect_.setAttribute('y', `${y}`); - this.markerSvgRect_.setAttribute('width', `${width}`); - this.markerSvgRect_.setAttribute('height', `${height}`); - this.currentMarkerSvg = this.markerSvgRect_; - } - - /** - * Flip the SVG paths in RTL. - * - * @param markerSvg The marker that we want to flip. - */ - private flipRtl(markerSvg: SVGElement) { - markerSvg.setAttribute('transform', 'scale(-1 1)'); - } - - /** Hide the marker. */ - hide() { - if ( - !this.markerSvgLine_ || - !this.markerSvgRect_ || - !this.markerInput_ || - !this.markerBlock_ - ) { - throw new Error('createDom should be called before hiding the marker'); - } - this.markerSvgLine_.style.display = 'none'; - this.markerSvgRect_.style.display = 'none'; - this.markerInput_.style.display = 'none'; - this.markerBlock_.style.display = 'none'; - } - - /** - * Fire event for the marker or marker. - * - * @param oldNode The old node the marker used to be on. - * @param curNode The new node the marker is currently on. - */ - protected fireMarkerEvent(oldNode: ASTNode, curNode: ASTNode) { - const curBlock = curNode.getSourceBlock(); - const event = new (eventUtils.get(EventType.MARKER_MOVE))( - curBlock, - this.isCursor(), - oldNode, - curNode, - ); - eventUtils.fire(event); - } - - /** - * Get the properties to make a marker blink. - * - * @returns The object holding attributes to make the marker blink. - */ - protected getBlinkProperties_(): {[key: string]: string} { - return { - 'attributeType': 'XML', - 'attributeName': 'fill', - 'dur': '1s', - 'values': this.colour_ + ';transparent;transparent;', - 'repeatCount': 'indefinite', - }; - } - - /** - * Create the marker SVG. - * - * @returns The SVG node created. - */ - protected createDomInternal_(): Element { - /* This markup will be generated and added to the .svgGroup_: - - - - - - */ - - this.markerSvg_ = dom.createSvgElement( - Svg.G, - { - 'width': this.constants_.CURSOR_WS_WIDTH, - 'height': this.constants_.WS_CURSOR_HEIGHT, - }, - this.svgGroup_, - ); - - // A horizontal line used to represent a workspace coordinate or next - // connection. - this.markerSvgLine_ = dom.createSvgElement( - Svg.RECT, - { - 'width': this.constants_.CURSOR_WS_WIDTH, - 'height': this.constants_.WS_CURSOR_HEIGHT, - }, - this.markerSvg_, - ); - - // A filled in rectangle used to represent a stack. - this.markerSvgRect_ = dom.createSvgElement( - Svg.RECT, - { - 'class': 'blocklyVerticalMarker', - 'rx': 10, - 'ry': 10, - }, - this.markerSvg_, - ); - - // A filled in puzzle piece used to represent an input value. - this.markerInput_ = dom.createSvgElement( - Svg.PATH, - {'transform': ''}, - this.markerSvg_, - ); - - // A path used to represent a previous connection and a block, an output - // connection and a block, or a block. - this.markerBlock_ = dom.createSvgElement( - Svg.PATH, - { - 'transform': '', - 'fill': 'none', - 'stroke-width': this.constants_.CURSOR_STROKE_WIDTH, - }, - this.markerSvg_, - ); - - this.hide(); - - // Markers and stack markers don't blink. - if (this.isCursor()) { - const blinkProperties = this.getBlinkProperties_(); - dom.createSvgElement(Svg.ANIMATE, blinkProperties, this.markerSvgLine_); - dom.createSvgElement(Svg.ANIMATE, blinkProperties, this.markerInput_); - dom.createSvgElement( - Svg.ANIMATE, - {...blinkProperties, attributeName: 'stroke'}, - this.markerBlock_, - ); - } - - return this.markerSvg_; - } - - /** - * Apply the marker's colour. - * - * @param _curNode The node that we want to draw the marker for. - */ - protected applyColour_(_curNode: ASTNode) { - if ( - !this.markerSvgLine_ || - !this.markerSvgRect_ || - !this.markerInput_ || - !this.markerBlock_ - ) { - throw new Error( - 'createDom should be called before applying color to the markerj', - ); - } - this.markerSvgLine_.setAttribute('fill', this.colour_); - this.markerSvgRect_.setAttribute('stroke', this.colour_); - this.markerInput_.setAttribute('fill', this.colour_); - this.markerBlock_.setAttribute('stroke', this.colour_); - - if (this.isCursor()) { - const values = this.colour_ + ';transparent;transparent;'; - this.markerSvgLine_.firstElementChild!.setAttribute('values', values); - this.markerInput_.firstElementChild!.setAttribute('values', values); - this.markerBlock_.firstElementChild!.setAttribute('values', values); - } - } - - /** Dispose of this marker. */ - dispose() { - if (this.svgGroup_) { - dom.removeNode(this.svgGroup_); - } - } -} diff --git a/core/renderers/zelos/marker_svg.ts b/core/renderers/zelos/marker_svg.ts deleted file mode 100644 index 395ece0b0be..00000000000 --- a/core/renderers/zelos/marker_svg.ts +++ /dev/null @@ -1,144 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -// Former goog.module ID: Blockly.zelos.MarkerSvg - -import type {BlockSvg} from '../../block_svg.js'; -import type {ASTNode} from '../../keyboard_nav/ast_node.js'; -import type {Marker} from '../../keyboard_nav/marker.js'; -import type {RenderedConnection} from '../../rendered_connection.js'; -import * as dom from '../../utils/dom.js'; -import {Svg} from '../../utils/svg.js'; -import type {WorkspaceSvg} from '../../workspace_svg.js'; -import type {ConstantProvider as BaseConstantProvider} from '../common/constants.js'; -import {MarkerSvg as BaseMarkerSvg} from '../common/marker_svg.js'; -import type {ConstantProvider as ZelosConstantProvider} from './constants.js'; - -/** - * Class to draw a marker. - */ -export class MarkerSvg extends BaseMarkerSvg { - // TODO(b/109816955): remove '!', see go/strict-prop-init-fix. - constants_!: ZelosConstantProvider; - - private markerCircle: SVGCircleElement | null = null; - - /** - * @param workspace The workspace the marker belongs to. - * @param constants The constants for the renderer. - * @param marker The marker to draw. - */ - constructor( - workspace: WorkspaceSvg, - constants: BaseConstantProvider, - marker: Marker, - ) { - super(workspace, constants, marker); - } - - /** - * Position and display the marker for an input or an output connection. - * - * @param curNode The node to draw the marker for. - */ - private showWithInputOutput(curNode: ASTNode) { - const block = curNode.getSourceBlock() as BlockSvg; - const connection = curNode.getLocation() as RenderedConnection; - const offsetInBlock = connection.getOffsetInBlock(); - - this.positionCircle(offsetInBlock.x, offsetInBlock.y); - this.setParent_(block); - this.showCurrent_(); - } - - override showWithOutput_(curNode: ASTNode) { - this.showWithInputOutput(curNode); - } - - override showWithInput_(curNode: ASTNode) { - this.showWithInputOutput(curNode); - } - - /** - * Draw a rectangle around the block. - * - * @param curNode The current node of the marker. - */ - override showWithBlock_(curNode: ASTNode) { - const block = curNode.getLocation() as BlockSvg; - - // Gets the height and width of entire stack. - const heightWidth = block.getHeightWidth(); - // Add padding so that being on a stack looks different than being on a - // block. - this.positionRect_(0, 0, heightWidth.width, heightWidth.height); - this.setParent_(block); - this.showCurrent_(); - } - - /** - * Position the circle we use for input and output connections. - * - * @param x The x position of the circle. - * @param y The y position of the circle. - */ - private positionCircle(x: number, y: number) { - this.markerCircle?.setAttribute('cx', `${x}`); - this.markerCircle?.setAttribute('cy', `${y}`); - this.currentMarkerSvg = this.markerCircle; - } - - override hide() { - super.hide(); - if (this.markerCircle) { - this.markerCircle.style.display = 'none'; - } - } - - override createDomInternal_() { - /* clang-format off */ - /* This markup will be generated and added to the .svgGroup_: - - - - - - */ - /* clang-format on */ - super.createDomInternal_(); - - this.markerCircle = dom.createSvgElement( - Svg.CIRCLE, - { - 'r': this.constants_.CURSOR_RADIUS, - 'stroke-width': this.constants_.CURSOR_STROKE_WIDTH, - }, - this.markerSvg_, - ); - this.hide(); - - // Markers and stack cursors don't blink. - if (this.isCursor()) { - const blinkProperties = this.getBlinkProperties_(); - dom.createSvgElement(Svg.ANIMATE, blinkProperties, this.markerCircle!); - } - - return this.markerSvg_!; - } - - override applyColour_(curNode: ASTNode) { - super.applyColour_(curNode); - - this.markerCircle?.setAttribute('fill', this.colour_); - this.markerCircle?.setAttribute('stroke', this.colour_); - - if (this.isCursor()) { - const values = this.colour_ + ';transparent;transparent;'; - this.markerCircle?.firstElementChild!.setAttribute('values', values); - } - } -} diff --git a/core/shortcut_items.ts b/core/shortcut_items.ts deleted file mode 100644 index 0db28a51a4b..00000000000 --- a/core/shortcut_items.ts +++ /dev/null @@ -1,341 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -// Former goog.module ID: Blockly.ShortcutItems - -import {BlockSvg} from './block_svg.js'; -import * as clipboard from './clipboard.js'; -import * as common from './common.js'; -import * as eventUtils from './events/utils.js'; -import {Gesture} from './gesture.js'; -import {ICopyData, isCopyable} from './interfaces/i_copyable.js'; -import {isDeletable} from './interfaces/i_deletable.js'; -import {isDraggable} from './interfaces/i_draggable.js'; -import {KeyboardShortcut, ShortcutRegistry} from './shortcut_registry.js'; -import {Coordinate} from './utils/coordinate.js'; -import {KeyCodes} from './utils/keycodes.js'; -import {Rect} from './utils/rect.js'; -import {WorkspaceSvg} from './workspace_svg.js'; - -/** - * Object holding the names of the default shortcut items. - */ -export enum names { - ESCAPE = 'escape', - DELETE = 'delete', - COPY = 'copy', - CUT = 'cut', - PASTE = 'paste', - UNDO = 'undo', - REDO = 'redo', -} - -/** - * Keyboard shortcut to hide chaff on escape. - */ -export function registerEscape() { - const escapeAction: KeyboardShortcut = { - name: names.ESCAPE, - preconditionFn(workspace) { - return !workspace.options.readOnly; - }, - callback(workspace) { - // AnyDuringMigration because: Property 'hideChaff' does not exist on - // type 'Workspace'. - (workspace as AnyDuringMigration).hideChaff(); - return true; - }, - keyCodes: [KeyCodes.ESC], - }; - ShortcutRegistry.registry.register(escapeAction); -} - -/** - * Keyboard shortcut to delete a block on delete or backspace - */ -export function registerDelete() { - const deleteShortcut: KeyboardShortcut = { - name: names.DELETE, - preconditionFn(workspace) { - const selected = common.getSelected(); - return ( - !workspace.options.readOnly && - selected != null && - isDeletable(selected) && - selected.isDeletable() && - !Gesture.inProgress() - ); - }, - callback(workspace, e) { - // 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(); - const selected = common.getSelected(); - if (selected instanceof BlockSvg) { - selected.checkAndDelete(); - } else if (isDeletable(selected) && selected.isDeletable()) { - eventUtils.setGroup(true); - selected.dispose(); - eventUtils.setGroup(false); - } - return true; - }, - keyCodes: [KeyCodes.DELETE, KeyCodes.BACKSPACE], - }; - ShortcutRegistry.registry.register(deleteShortcut); -} - -let copyData: ICopyData | null = null; -let copyWorkspace: WorkspaceSvg | null = null; -let copyCoords: Coordinate | null = null; - -/** - * Keyboard shortcut to copy a block on ctrl+c, cmd+c, or alt+c. - */ -export function registerCopy() { - const ctrlC = ShortcutRegistry.registry.createSerializedKey(KeyCodes.C, [ - KeyCodes.CTRL, - ]); - const altC = ShortcutRegistry.registry.createSerializedKey(KeyCodes.C, [ - KeyCodes.ALT, - ]); - const metaC = ShortcutRegistry.registry.createSerializedKey(KeyCodes.C, [ - KeyCodes.META, - ]); - - const copyShortcut: KeyboardShortcut = { - name: names.COPY, - preconditionFn(workspace) { - const selected = common.getSelected(); - return ( - !workspace.options.readOnly && - !Gesture.inProgress() && - selected != null && - isDeletable(selected) && - selected.isDeletable() && - isDraggable(selected) && - selected.isMovable() && - isCopyable(selected) - ); - }, - callback(workspace, e) { - // Prevent the default copy behavior, which may beep or otherwise indicate - // an error due to the lack of a selection. - e.preventDefault(); - workspace.hideChaff(); - const selected = common.getSelected(); - if (!selected || !isCopyable(selected)) return false; - copyData = selected.toCopyData(); - copyWorkspace = - selected.workspace instanceof WorkspaceSvg - ? selected.workspace - : workspace; - copyCoords = isDraggable(selected) - ? selected.getRelativeToSurfaceXY() - : null; - return !!copyData; - }, - keyCodes: [ctrlC, altC, metaC], - }; - ShortcutRegistry.registry.register(copyShortcut); -} - -/** - * Keyboard shortcut to copy and delete a block on ctrl+x, cmd+x, or alt+x. - */ -export function registerCut() { - const ctrlX = ShortcutRegistry.registry.createSerializedKey(KeyCodes.X, [ - KeyCodes.CTRL, - ]); - const altX = ShortcutRegistry.registry.createSerializedKey(KeyCodes.X, [ - KeyCodes.ALT, - ]); - const metaX = ShortcutRegistry.registry.createSerializedKey(KeyCodes.X, [ - KeyCodes.META, - ]); - - const cutShortcut: KeyboardShortcut = { - name: names.CUT, - preconditionFn(workspace) { - const selected = common.getSelected(); - return ( - !workspace.options.readOnly && - !Gesture.inProgress() && - selected != null && - isDeletable(selected) && - selected.isDeletable() && - isDraggable(selected) && - selected.isMovable() && - !selected.workspace!.isFlyout - ); - }, - callback(workspace) { - const selected = common.getSelected(); - - if (selected instanceof BlockSvg) { - copyData = selected.toCopyData(); - copyWorkspace = workspace; - copyCoords = selected.getRelativeToSurfaceXY(); - selected.checkAndDelete(); - return true; - } else if ( - isDeletable(selected) && - selected.isDeletable() && - isCopyable(selected) - ) { - copyData = selected.toCopyData(); - copyWorkspace = workspace; - copyCoords = isDraggable(selected) - ? selected.getRelativeToSurfaceXY() - : null; - selected.dispose(); - return true; - } - return false; - }, - keyCodes: [ctrlX, altX, metaX], - }; - - ShortcutRegistry.registry.register(cutShortcut); -} - -/** - * Keyboard shortcut to paste a block on ctrl+v, cmd+v, or alt+v. - */ -export function registerPaste() { - const ctrlV = ShortcutRegistry.registry.createSerializedKey(KeyCodes.V, [ - KeyCodes.CTRL, - ]); - const altV = ShortcutRegistry.registry.createSerializedKey(KeyCodes.V, [ - KeyCodes.ALT, - ]); - const metaV = ShortcutRegistry.registry.createSerializedKey(KeyCodes.V, [ - KeyCodes.META, - ]); - - const pasteShortcut: KeyboardShortcut = { - name: names.PASTE, - preconditionFn(workspace) { - return !workspace.options.readOnly && !Gesture.inProgress(); - }, - callback() { - if (!copyData || !copyWorkspace) return false; - if (!copyCoords) { - // If we don't have location data about the original copyable, let the - // paster determine position. - return !!clipboard.paste(copyData, copyWorkspace); - } - - const {left, top, width, height} = copyWorkspace - .getMetricsManager() - .getViewMetrics(true); - const viewportRect = new Rect(top, top + height, left, left + width); - - if (viewportRect.contains(copyCoords.x, copyCoords.y)) { - // If the original copyable is inside the viewport, let the paster - // determine position. - return !!clipboard.paste(copyData, copyWorkspace); - } - - // Otherwise, paste in the middle of the viewport. - const centerCoords = new Coordinate(left + width / 2, top + height / 2); - return !!clipboard.paste(copyData, copyWorkspace, centerCoords); - }, - keyCodes: [ctrlV, altV, metaV], - }; - - ShortcutRegistry.registry.register(pasteShortcut); -} - -/** - * Keyboard shortcut to undo the previous action on ctrl+z, cmd+z, or alt+z. - */ -export function registerUndo() { - const ctrlZ = ShortcutRegistry.registry.createSerializedKey(KeyCodes.Z, [ - KeyCodes.CTRL, - ]); - const altZ = ShortcutRegistry.registry.createSerializedKey(KeyCodes.Z, [ - KeyCodes.ALT, - ]); - const metaZ = ShortcutRegistry.registry.createSerializedKey(KeyCodes.Z, [ - KeyCodes.META, - ]); - - const undoShortcut: KeyboardShortcut = { - name: names.UNDO, - preconditionFn(workspace) { - return !workspace.options.readOnly && !Gesture.inProgress(); - }, - callback(workspace, e) { - // 'z' for undo 'Z' is for redo. - (workspace as WorkspaceSvg).hideChaff(); - workspace.undo(false); - e.preventDefault(); - return true; - }, - keyCodes: [ctrlZ, altZ, metaZ], - }; - ShortcutRegistry.registry.register(undoShortcut); -} - -/** - * Keyboard shortcut to redo the previous action on ctrl+shift+z, cmd+shift+z, - * or alt+shift+z. - */ -export function registerRedo() { - const ctrlShiftZ = ShortcutRegistry.registry.createSerializedKey(KeyCodes.Z, [ - KeyCodes.SHIFT, - KeyCodes.CTRL, - ]); - const altShiftZ = ShortcutRegistry.registry.createSerializedKey(KeyCodes.Z, [ - KeyCodes.SHIFT, - KeyCodes.ALT, - ]); - const metaShiftZ = ShortcutRegistry.registry.createSerializedKey(KeyCodes.Z, [ - KeyCodes.SHIFT, - KeyCodes.META, - ]); - // Ctrl-y is redo in Windows. Command-y is never valid on Macs. - const ctrlY = ShortcutRegistry.registry.createSerializedKey(KeyCodes.Y, [ - KeyCodes.CTRL, - ]); - - const redoShortcut: KeyboardShortcut = { - name: names.REDO, - preconditionFn(workspace) { - return !Gesture.inProgress() && !workspace.options.readOnly; - }, - callback(workspace, e) { - // 'z' for undo 'Z' is for redo. - (workspace as WorkspaceSvg).hideChaff(); - workspace.undo(true); - e.preventDefault(); - return true; - }, - keyCodes: [ctrlShiftZ, altShiftZ, metaShiftZ, ctrlY], - }; - ShortcutRegistry.registry.register(redoShortcut); -} - -/** - * Registers all default keyboard shortcut item. This should be called once per - * instance of KeyboardShortcutRegistry. - * - * @internal - */ -export function registerDefaultShortcuts() { - registerEscape(); - registerDelete(); - registerCopy(); - registerCut(); - registerPaste(); - registerUndo(); - registerRedo(); -} - -registerDefaultShortcuts(); diff --git a/core/variable_model.ts b/core/variable_model.ts deleted file mode 100644 index 58e48f36268..00000000000 --- a/core/variable_model.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * Components for the variable model. - * - * @class - */ -// Former goog.module ID: Blockly.VariableModel - -// Unused import preserved for side-effects. Remove if unneeded. -import './events/events_var_create.js'; - -import * as idGenerator from './utils/idgenerator.js'; -import type {Workspace} from './workspace.js'; - -/** - * Class for a variable model. - * Holds information for the variable including name, ID, and type. - * - * @see {Blockly.FieldVariable} - */ -export class VariableModel { - type: string; - private readonly id: string; - - /** - * @param workspace The variable's workspace. - * @param name The name of the variable. This is the user-visible name (e.g. - * 'my var' or '私の変数'), not the generated name. - * @param opt_type The type of the variable like 'int' or 'string'. - * Does not need to be unique. Field_variable can filter variables based - * on their type. This will default to '' which is a specific type. - * @param opt_id The unique ID of the variable. This will default to a UUID. - */ - constructor( - public workspace: Workspace, - public name: string, - opt_type?: string, - opt_id?: string, - ) { - /** - * The type of the variable, such as 'int' or 'sound_effect'. This may be - * used to build a list of variables of a specific type. By default this is - * the empty string '', which is a specific type. - * - * @see {Blockly.FieldVariable} - */ - this.type = opt_type || ''; - - /** - * A unique ID for the variable. This should be defined at creation and - * not change, even if the name changes. In most cases this should be a - * UUID. - */ - this.id = opt_id || idGenerator.genUid(); - } - - /** @returns The ID for the variable. */ - getId(): string { - return this.id; - } - - /** - * A custom compare function for the VariableModel objects. - * - * @param var1 First variable to compare. - * @param var2 Second variable to compare. - * @returns -1 if name of var1 is less than name of var2, 0 if equal, and 1 if - * greater. - * @internal - */ - static compareByName(var1: VariableModel, var2: VariableModel): number { - return var1.name.localeCompare(var2.name, undefined, {sensitivity: 'base'}); - } -} diff --git a/core/workspace_audio.ts b/core/workspace_audio.ts deleted file mode 100644 index 1759b30edbb..00000000000 --- a/core/workspace_audio.ts +++ /dev/null @@ -1,171 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * Object in charge of loading, storing, and playing audio for a - * workspace. - * - * @class - */ -// Former goog.module ID: Blockly.WorkspaceAudio - -import * as userAgent from './utils/useragent.js'; -import type {WorkspaceSvg} from './workspace_svg.js'; - -/** - * Prevent a sound from playing if another sound preceded it within this many - * milliseconds. - */ -const SOUND_LIMIT = 100; - -/** - * Class for loading, storing, and playing audio for a workspace. - */ -export class WorkspaceAudio { - /** Database of pre-loaded sounds. */ - private sounds = new Map(); - - /** Time that the last sound was played. */ - private lastSound: Date | null = null; - - /** Whether the audio is muted or not. */ - private muted: boolean = false; - - /** - * @param parentWorkspace The parent of the workspace this audio object - * belongs to, or null. - */ - constructor(private parentWorkspace: WorkspaceSvg) {} - - /** - * Dispose of this audio manager. - * - * @internal - */ - dispose() { - this.sounds.clear(); - } - - /** - * Load an audio file. Cache it, ready for instantaneous playing. - * - * @param filenames List of file types in decreasing order of preference (i.e. - * increasing size). E.g. ['media/go.mp3', 'media/go.wav'] Filenames - * include path from Blockly's root. File extensions matter. - * @param name Name of sound. - */ - load(filenames: string[], name: string) { - if (!filenames.length) { - return; - } - let audioTest; - try { - audioTest = new globalThis['Audio'](); - } catch { - // No browser support for Audio. - // IE can throw an error even if the Audio object exists. - return; - } - let sound; - for (let i = 0; i < filenames.length; i++) { - const filename = filenames[i]; - const ext = filename.match(/\.(\w+)$/); - if (ext && audioTest.canPlayType('audio/' + ext[1])) { - // Found an audio format we can play. - sound = new globalThis['Audio'](filename); - break; - } - } - if (sound) { - this.sounds.set(name, sound); - } - } - - /** - * Preload all the audio files so that they play quickly when asked for. - * - * @internal - */ - preload() { - for (const sound of this.sounds.values()) { - sound.volume = 0.01; - const playPromise = sound.play(); - // Edge does not return a promise, so we need to check. - if (playPromise !== undefined) { - // If we don't wait for the play request to complete before calling - // pause() we will get an exception: (DOMException: The play() request - // was interrupted) See more: - // https://developers.google.com/web/updates/2017/06/play-request-was-interrupted - playPromise.then(sound.pause).catch( - // Play without user interaction was prevented. - function () {}, - ); - } else { - sound.pause(); - } - - // iOS can only process one sound at a time. Trying to load more than one - // corrupts the earlier ones. Just load one and leave the others - // uncached. - if (userAgent.IPAD || userAgent.IPHONE) { - break; - } - } - } - - /** - * Play a named sound at specified volume. If volume is not specified, - * use full volume (1). - * - * @param name Name of sound. - * @param opt_volume Volume of sound (0-1). - */ - play(name: string, opt_volume?: number) { - if (this.muted) { - return; - } - const sound = this.sounds.get(name); - if (sound) { - // Don't play one sound on top of another. - const now = new Date(); - if ( - this.lastSound !== null && - now.getTime() - this.lastSound.getTime() < SOUND_LIMIT - ) { - return; - } - this.lastSound = now; - let mySound; - if (userAgent.IPAD || userAgent.ANDROID) { - // Creating a new audio node causes lag in Android and iPad. Android - // refetches the file from the server, iPad uses a singleton audio - // node which must be deleted and recreated for each new audio tag. - mySound = sound; - } else { - mySound = sound.cloneNode() as HTMLAudioElement; - } - mySound.volume = opt_volume === undefined ? 1 : opt_volume; - mySound.play(); - } else if (this.parentWorkspace) { - // Maybe a workspace on a lower level knows about this sound. - this.parentWorkspace.getAudioManager().play(name, opt_volume); - } - } - - /** - * @param muted If true, mute sounds. Otherwise, play them. - */ - setMuted(muted: boolean) { - this.muted = muted; - } - - /** - * @returns Whether the audio is currently muted or not. - */ - getMuted(): boolean { - return this.muted; - } -} diff --git a/demos/minimap/icon.png b/demos/minimap/icon.png deleted file mode 100644 index 870caa070d8..00000000000 Binary files a/demos/minimap/icon.png and /dev/null differ diff --git a/demos/minimap/index.html b/demos/minimap/index.html deleted file mode 100644 index 1a8fd357dea..00000000000 --- a/demos/minimap/index.html +++ /dev/null @@ -1,91 +0,0 @@ - - - - - Blockly Demo: Minimap - - - - - - - -

Blockly > - Demos > Minimap

- -

This is a simple demo showing how a minimap can be implemented.

- - - - - - -
-
-
-
-
- - - - - - - - diff --git a/demos/minimap/minimap.js b/demos/minimap/minimap.js deleted file mode 100644 index a4343899ad6..00000000000 --- a/demos/minimap/minimap.js +++ /dev/null @@ -1,302 +0,0 @@ -/** - - * Copyright 2017 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview JavaScript for Blockly's Minimap demo. - */ -'use strict'; - -/** - * Creating a separate namespace for minimap. - */ -var Minimap = {}; - -/** - * Initialize the workspace and minimap. - * @param {!Workspace} workspace The main workspace of the user. - * @param {!Workspace} minimap The workspace that will be used as a minimap. - */ -Minimap.init = function(workspace, minimap) { - this.workspace = workspace; - this.minimap = minimap; - - // Adding scroll callback functionality to vScroll and hScroll just for this demo. - // IMPORTANT: This should be changed when there is proper UI event handling - // API available and should be handled by workspace's event listeners. - this.workspace.scrollbar.vScroll.setHandlePosition = function(newPosition) { - this.handlePosition_ = newPosition; - this.svgHandle_.setAttribute(this.positionAttribute_, this.handlePosition_); - - // Code above is same as the original setHandlePosition function in core/scrollbar.js. - // New code starts from here. - - // Get the absolutePosition. - var absolutePosition = (this.handlePosition_ / this.ratio); - - // Firing the scroll change listener. - Minimap.onScrollChange(absolutePosition, this.horizontal_); - }; - - // Adding call back for horizontal scroll. - this.workspace.scrollbar.hScroll.setHandlePosition = function(newPosition) { - this.handlePosition_ = newPosition; - this.svgHandle_.setAttribute(this.positionAttribute_, this.handlePosition_); - - // Code above is same as the original setHandlePosition function in core/scrollbar.js. - // New code starts from here. - - // Get the absolutePosition. - var absolutePosition = (this.handlePosition_ / this.ratio); - - // Firing the scroll change listener. - Minimap.onScrollChange(absolutePosition, this.horizontal_); - }; - - - // Required to stop a positive feedback loop when user clicks minimap - // and the scroll changes, which in turn may change minimap. - this.disableScrollChange = false; - - // Listen to events on the main workspace. - this.workspace.addChangeListener(Minimap.mirrorEvent); - - //Get rectangle bounding the minimap div. - this.rect = document.getElementById('mapDiv').getBoundingClientRect(); - - // Create a svg overlay on the top of mapDiv for the minimap. - this.svg = Blockly.utils.dom.createSvgElement('svg', { - 'xmlns': Blockly.utils.dom.SVG_NS, - 'xmlns:html': Blockly.utils.dom.HTML_NS, - 'xmlns:xlink': Blockly.utils.dom.XLINK_NS, - 'version': '1.1', - 'height': this.rect.bottom-this.rect.top, - 'width': this.rect.right-this.rect.left, - 'class': 'minimap', - }, document.getElementById('mapDiv')); - this.svg.style.top = this.rect.top + 'px'; - this.svg.style.left = this.rect.left + 'px'; - - // Creating a rectangle in the minimap that represents current view. - Blockly.utils.dom.createSvgElement('rect', { - 'width': 100, - 'height': 100, - 'class': 'mapDragger' - }, this.svg); - - // Rectangle in the minimap that represents current view. - this.mapDragger = this.svg.childNodes[0]; - - // Adding mouse events to the rectangle, to make it Draggable. - // Using Blockly.browserEvents.bind to attach mouse/touch listeners. - Blockly.browserEvents.bind( - this.mapDragger, 'mousedown', null, Minimap.mousedown); - - //When the window change, we need to resize the minimap window. - window.addEventListener('resize', Minimap.repositionMinimap); - - // Mouse up event for the minimap. - this.svg.addEventListener('mouseup', Minimap.updateMapDragger); - - //Boolean to check whether I am dragging the surface or not. - this.isDragging = false; -}; - -Minimap.mousedown = function(e) { - // Using Blockly.browserEvents.bind to attach mouse/touch listeners. - Minimap.mouseMoveBindData = Blockly.browserEvents.bind( - document, 'mousemove', null, Minimap.mousemove); - Minimap.mouseUpBindData = - Blockly.browserEvents.bind(document, 'mouseup', null, Minimap.mouseup); - - Minimap.isDragging = true; - e.stopPropagation(); -}; - -Minimap.mouseup = function(e) { - Minimap.isDragging = false; - // Removing listeners. - Blockly.browserEvents.unbind(Minimap.mouseUpBindData); - Blockly.browserEvents.unbind(Minimap.mouseMoveBindData); - Minimap.updateMapDragger(e); - e.stopPropagation(); -}; - -Minimap.mousemove = function(e) { - if (Minimap.isDragging) { - Minimap.updateMapDragger(e); - e.stopPropagation(); - } -}; - -/** - * Run non-UI events from the main workspace on the minimap. - * @param {!Blockly.Events.Abstract} event Event that triggered in the main - * workspace. - */ -Minimap.mirrorEvent = function(event) { - if (event.isUiEvent) { - return; // Don't mirror UI events. - } - // Convert event to JSON. This could then be transmitted across the net. - var json = event.toJson(); - // Convert JSON back into an event, then execute it. - var minimapEvent = Blockly.Events.fromJson(json, Minimap.minimap); - minimapEvent.run(true); - Minimap.scaleMinimap(); - Minimap.setDraggerHeight(); - Minimap.setDraggerWidth(); -}; - -/** - * Called when window is resized. Repositions the minimap overlay. - */ -Minimap.repositionMinimap = function() { - Minimap.rect = document.getElementById('mapDiv').getBoundingClientRect(); - Minimap.svg.style.top = Minimap.rect.top + 'px'; - Minimap.svg.style.left = Minimap.rect.left + 'px'; -}; - -/** - * Updates the rectangle's height. - */ -Minimap.setDraggerHeight = function() { - var workspaceMetrics = Minimap.workspace.getMetrics(); - var draggerHeight = (workspaceMetrics.viewHeight / Minimap.workspace.scale) * - Minimap.minimap.scale; - // It's zero when first block is placed. - if (draggerHeight === 0) { - return; - } - Minimap.mapDragger.setAttribute('height', draggerHeight); -}; - -/** - * Updates the rectangle's width. - */ -Minimap.setDraggerWidth = function() { - var workspaceMetrics = Minimap.workspace.getMetrics(); - var draggerWidth = (workspaceMetrics.viewWidth / Minimap.workspace.scale) * - Minimap.minimap.scale; - // It's zero when first block is placed. - if (draggerWidth === 0) { - return; - } - Minimap.mapDragger.setAttribute('width', draggerWidth); -}; - - -/** - * Updates the overall position of the viewport of the minimap by appropriately - * using translate functions. - */ -Minimap.scaleMinimap = function() { - var minimapBoundingBox = Minimap.minimap.getBlocksBoundingBox(); - var workspaceBoundingBox = Minimap.workspace.getBlocksBoundingBox(); - var workspaceMetrics = Minimap.workspace.getMetrics(); - var minimapMetrics = Minimap.minimap.getMetrics(); - - // Scaling the minimap such that all the blocks can be seen in the viewport. - // This padding is default because this is how to scrollbar(in main workspace) - // is implemented. - var topPadding = (workspaceMetrics.viewHeight) * Minimap.minimap.scale / - (2 * Minimap.workspace.scale); - var sidePadding = (workspaceMetrics.viewWidth) * Minimap.minimap.scale / - (2 * Minimap.workspace.scale); - - // If actual padding is more than half view ports height, - // change it to actual padding. - if ((workspaceBoundingBox.y * Minimap.workspace.scale - - workspaceMetrics.contentTop) * - Minimap.minimap.scale / Minimap.workspace.scale > topPadding) { - topPadding = (workspaceBoundingBox.y * Minimap.workspace.scale - - workspaceMetrics.contentTop) * - Minimap.minimap.scale / Minimap.workspace.scale; - } - - // If actual padding is more than half view ports height, - // change it to actual padding. - if ((workspaceBoundingBox.x * Minimap.workspace.scale - - workspaceMetrics.contentLeft) * - Minimap.minimap.scale / Minimap.workspace.scale > sidePadding) { - sidePadding = (workspaceBoundingBox.x * Minimap.workspace.scale - - workspaceMetrics.contentLeft) * - Minimap.minimap.scale / Minimap.workspace.scale; - } - - var scalex = (minimapMetrics.viewWidth - 2 * sidePadding) / - minimapBoundingBox.width; - var scaley = (minimapMetrics.viewHeight - 2 * topPadding) / - minimapBoundingBox.height; - Minimap.minimap.setScale(Math.min(scalex, scaley)); - - // Translating the minimap. - Minimap.minimap.translate( - -minimapMetrics.contentLeft * Minimap.minimap.scale + sidePadding, - -minimapMetrics.contentTop * Minimap.minimap.scale + topPadding); -}; - -/** - * Handles the onclick event on the minimapBoundingBox. - * Changes mapDraggers position. - * @param {!Event} e Event from the mouse click. - */ -Minimap.updateMapDragger = function(e) { - var y = e.clientY; - var x = e.clientX; - var draggerHeight = Minimap.mapDragger.getAttribute('height'); - var draggerWidth = Minimap.mapDragger.getAttribute('width'); - - var finalY = y - Minimap.rect.top - draggerHeight / 2; - var finalX = x - Minimap.rect.left - draggerWidth / 2; - - var maxValidY = (Minimap.workspace.getMetrics().contentHeight - - Minimap.workspace.getMetrics().viewHeight) * Minimap.minimap.scale; - var maxValidX = (Minimap.workspace.getMetrics().contentWidth - - Minimap.workspace.getMetrics().viewWidth) * Minimap.minimap.scale; - - if (y + draggerHeight / 2 > Minimap.rect.bottom) { - finalY = Minimap.rect.bottom - Minimap.rect.top - draggerHeight; - } else if (y < Minimap.rect.top + draggerHeight / 2) { - finalY = 0; - } - - if (x + draggerWidth / 2 > Minimap.rect.right) { - finalX = Minimap.rect.right - Minimap.rect.left - draggerWidth; - } else if (x < Minimap.rect.left + draggerWidth / 2) { - finalX = 0; - } - - // Do not go below lower bound of scrollbar. - if (finalY > maxValidY) { - finalY = maxValidY; - } - if (finalX > maxValidX) { - finalX = maxValidX; - } - Minimap.mapDragger.setAttribute('y', finalY); - Minimap.mapDragger.setAttribute('x', finalX); - // Required, otherwise creates a feedback loop. - Minimap.disableScrollChange = true; - Minimap.workspace.scrollbar.vScroll.set((finalY * Minimap.workspace.scale) / - Minimap.minimap.scale); - Minimap.workspace.scrollbar.hScroll.set((finalX * Minimap.workspace.scale) / - Minimap.minimap.scale); - Minimap.disableScrollChange = false; -}; - -/** - * Handles the onclick event on the minimapBoundingBox, parameters are passed by - * the event handler. - * @param {number} position This is the absolute position of the scrollbar. - * @param {boolean} horizontal Informs if the change event if for - * horizontal (true) or vertical (false) scrollbar. - */ -Minimap.onScrollChange = function(position, horizontal) { - if (!Minimap.disableScrollChange) { - Minimap.mapDragger.setAttribute(horizontal ? 'x' : 'y', - position * Minimap.minimap.scale / Minimap.workspace.scale); - } -}; diff --git a/demos/mobile/README.md b/demos/mobile/README.md deleted file mode 100644 index 168690e55db..00000000000 --- a/demos/mobile/README.md +++ /dev/null @@ -1,53 +0,0 @@ -# Blockly on Mobile Devices - -This directory contains three examples of running the Blockly library on mobile -devices. The `html/` directory is a example of configuring a webpage for touch -devices, with a Blockly workspace that fills the screen. - -The `mobile/html/` is also the basis for the Android and iOS demos. Each native -app copies this demo into the app's local resources, and required Blockly -library files, and hosts them in an embedded WebView. - -Thus, developers can quickly iterate within the `mobile/html/` directory, and -see changes in both the Android and iOS native apps. - -## Running the Mobile HTML Demo - -Before running the mobile HTML demo, you need to create some symbolic links -in your local file system. Run the `mobile/html/ln_resources.sh` file from -the `mobile/html/` directory. This mimics the relative locations of the -Blockly files seen when loading the page in a native app's embedded WebView. - -After doing this, opening `mobile/html/index.html` should open normally, -filling the page with one large Blockly workspace. - -## The Android App - -### Build and Run - -Open the `demos/mobile/android/` directory in Android Studio. The project -files in the directory should be ready to build and run the demo in an emulator -or connected device. - -### Android Copy Tasks - -If you edit the `mobile/html/` demo to include new files, you will need to -update the native app project files to also copy those files. - -In the Android project, two Gradle tasks are responsible for the copies. -In `mobile/android/app/build.gradle`, the tasks `copyBlocklyHtmlFile` and -`copyBlocklyMoreFiles` configure the copy actions. - -## The iOS App - -### Build and Run - -Open the `demos/mobile/iOS/` directory in XCode. The project files in the -directory should be ready to build and run the demo in a simulator or connected -device. - -### iOS Copy Script - -The XCode project call out to `mobile/ios/cp_resources.sh` to copy the required -HTML and related files. If you've edited the `mobile/html/` demo to require new -files, update this script to copy these files, too. diff --git a/demos/mobile/android/.gitignore b/demos/mobile/android/.gitignore deleted file mode 100644 index 3ba2b2b61bd..00000000000 --- a/demos/mobile/android/.gitignore +++ /dev/null @@ -1,27 +0,0 @@ -/build -/captures -/app/src/main/assets/blockly -.settings -.project - -# Local Settings -local.properties - -# Project files -*.komodoproject -.gradle -*.iml -.idea - -# Build files -*.pyc -*.apk -*.ap_ -*.class -*.dex - -# OSX Files -.DS_Store - -# Windows Files -Thumb.db diff --git a/demos/mobile/android/README.md b/demos/mobile/android/README.md deleted file mode 100644 index 31f968feab4..00000000000 --- a/demos/mobile/android/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# Blockly in an Android WebView - -This code demonstrates how to get Blockly running in an Android app by -embedding it in a WebView. - -### BlocklyWebViewFragment - -Most of the work is done within the fragment class `BlocklyWebViewFragment`. -This fragment instantiates the WebView, loads the HTML -(`assets/blockly/webview.html`, copied from `demos/mobile/html/index.html`), -and provides a few helper methods. - -### Copying web assets with gradle - -This android project copies the necessary files from the main Blockly -repository (i.e., parent directory). In `app/build.gradle`, note the -`copyBlocklyHtmlFile` and `copyBlocklyMoreFiles` tasks. - -In your own project, the HTML and related files can be placed directly in the -`assets/blockly` directory without the copy step. However, using the copy tasks -simplifies the synchronization with an iOS app using the same files. - -### Loading Block Definitions and Generator functions - -The `webview.html` loads the block definitions and generator functions directly -into the page, without support or coordination with the Android classes. This -assumes the app will always utilize the same blocks. This does not mean all -blocks are visible to the user all the time; that is controlled by the toolbox -and workspace files. This should accommodate almost all applications. - -This does mean loading your own block definitions and generators will involve -editing the HTML, adding you own ` - - - - - - -
- - - diff --git a/demos/mobile/html/ln_resources.sh b/demos/mobile/html/ln_resources.sh deleted file mode 100755 index 8f7a8b979bd..00000000000 --- a/demos/mobile/html/ln_resources.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -e -# -# Create symbolic links in this directory for the -# Blockly library files used by this demo's index.html. - -if [[ ! -e ../../../dist/blockly_compressed.js ]]; then - echo "ERROR: Could not locate blockly_compressed.js. Run from demos/mobile/html/" 1>&2 - exit 1 # terminate and indicate error -fi - -if [ ! -L blockly_compressed.js ]; then - ln -s ../../../dist/blockly_compressed.js blockly_compressed.js -fi -if [ ! -L blocks_compressed.js ]; then - ln -s ../../../dist/blocks_compressed.js blocks_compressed.js -fi -if [ ! -L media ]; then - ln -s ../../../media media -fi -if [ ! -L msg ]; then - ln -s ../../../build/msg msg -fi diff --git a/demos/mobile/html/toolbox_standard.js b/demos/mobile/html/toolbox_standard.js deleted file mode 100644 index 20326882fc5..00000000000 --- a/demos/mobile/html/toolbox_standard.js +++ /dev/null @@ -1,333 +0,0 @@ - -var BLOCKLY_TOOLBOX_XML = BLOCKLY_TOOLBOX_XML || Object.create(null); - -/* BEGINNING BLOCKLY_TOOLBOX_XML ASSIGNMENT. DO NOT EDIT. USE BLOCKLY DEVTOOLS. */ -BLOCKLY_TOOLBOX_XML['standard'] = -// From XML string/file, replace ^\s?(\s*)?(<.*>)$ with \+$1'$2' -// Tweak first and last line. -'' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '10' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '1' -+ '' -+ '' -+ '' -+ '' -+ '10' -+ '' -+ '' -+ '' -+ '' -+ '1' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '123' -+ '' -+ '' -+ '' -+ '' -+ '1' -+ '' -+ '' -+ '' -+ '' -+ '1' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '9' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '45' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '0' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '3.1' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '64' -+ '' -+ '' -+ '' -+ '' -+ '10' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '50' -+ '' -+ '' -+ '' -+ '' -+ '1' -+ '' -+ '' -+ '' -+ '' -+ '100' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '1' -+ '' -+ '' -+ '' -+ '' -+ '100' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ 'abc' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ 'text' -+ '' -+ '' -+ '' -+ '' -+ 'abc' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ 'text' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ 'text' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ 'abc' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ 'abc' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ 'abc' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ 'abc' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '5' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ 'list' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ 'list' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ 'list' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ 'list' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ ',' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '100' -+ '' -+ '' -+ '' -+ '' -+ '50' -+ '' -+ '' -+ '' -+ '' -+ '0' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '#ff0000' -+ '' -+ '' -+ '' -+ '' -+ '#3333ff' -+ '' -+ '' -+ '' -+ '' -+ '0.5' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ '' -+ ''; -/* END BLOCKLY_TOOLBOX_XML ASSIGNMENT. DO NOT EDIT. */ diff --git a/demos/mobile/ios/.gitignore b/demos/mobile/ios/.gitignore deleted file mode 100644 index 6c1f837691d..00000000000 --- a/demos/mobile/ios/.gitignore +++ /dev/null @@ -1,25 +0,0 @@ -# Files copied by cp_resources.sh -/Resources/Non-Localized/Blockly - - -# Xcode.gitignore - -## User settings -xcuserdata/ - -## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) -*.xcscmblueprint -*.xccheckout - -## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) -build/ -DerivedData/ -*.moved-aside -*.pbxuser -!default.pbxuser -*.mode1v3 -!default.mode1v3 -*.mode2v3 -!default.mode2v3 -*.perspectivev3 -!default.perspectivev3 diff --git a/demos/mobile/ios/Blockly WebView.xcodeproj/project.pbxproj b/demos/mobile/ios/Blockly WebView.xcodeproj/project.pbxproj deleted file mode 100644 index a3f72fcd431..00000000000 --- a/demos/mobile/ios/Blockly WebView.xcodeproj/project.pbxproj +++ /dev/null @@ -1,390 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 50; - objects = { - -/* Begin PBXBuildFile section */ - AB036C55211B89D600CCC9D8 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB036C54211B89D600CCC9D8 /* WebKit.framework */; }; - AB980111211A37B50025AFF2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB980110211A37B50025AFF2 /* AppDelegate.swift */; }; - AB980113211A37B50025AFF2 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB980112211A37B50025AFF2 /* ViewController.swift */; }; - AB980116211A37B50025AFF2 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AB980114211A37B50025AFF2 /* Main.storyboard */; }; - AB980118211A37B70025AFF2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AB980117211A37B70025AFF2 /* Assets.xcassets */; }; - AB98011B211A37B70025AFF2 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AB980119211A37B70025AFF2 /* LaunchScreen.storyboard */; }; - ABA1B7FC212214E7000D3CC5 /* Blockly in Resources */ = {isa = PBXBuildFile; fileRef = ABA1B7FB212214E7000D3CC5 /* Blockly */; }; -/* End PBXBuildFile section */ - -/* Begin PBXFileReference section */ - AB036C54211B89D600CCC9D8 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; - AB98010D211A37B50025AFF2 /* Blockly WebView.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Blockly WebView.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - AB980110211A37B50025AFF2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - AB980112211A37B50025AFF2 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; - AB980115211A37B50025AFF2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - AB980117211A37B70025AFF2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - AB98011A211A37B70025AFF2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - AB98011C211A37B70025AFF2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - ABA1B7FB212214E7000D3CC5 /* Blockly */ = {isa = PBXFileReference; lastKnownFileType = folder; name = Blockly; path = "Resources/Non-Localized/Blockly"; sourceTree = SOURCE_ROOT; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - AB98010A211A37B50025AFF2 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - AB036C55211B89D600CCC9D8 /* WebKit.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - AB036C53211B89D500CCC9D8 /* Frameworks */ = { - isa = PBXGroup; - children = ( - AB036C54211B89D600CCC9D8 /* WebKit.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; - AB980104211A37B50025AFF2 = { - isa = PBXGroup; - children = ( - AB98010F211A37B50025AFF2 /* Blockly WebView */, - AB98010E211A37B50025AFF2 /* Products */, - AB036C53211B89D500CCC9D8 /* Frameworks */, - ); - sourceTree = ""; - }; - AB98010E211A37B50025AFF2 /* Products */ = { - isa = PBXGroup; - children = ( - AB98010D211A37B50025AFF2 /* Blockly WebView.app */, - ); - name = Products; - sourceTree = ""; - }; - AB98010F211A37B50025AFF2 /* Blockly WebView */ = { - isa = PBXGroup; - children = ( - ABA1B7F9212214B9000D3CC5 /* Resources */, - AB980110211A37B50025AFF2 /* AppDelegate.swift */, - AB980112211A37B50025AFF2 /* ViewController.swift */, - AB980114211A37B50025AFF2 /* Main.storyboard */, - AB980117211A37B70025AFF2 /* Assets.xcassets */, - AB980119211A37B70025AFF2 /* LaunchScreen.storyboard */, - AB98011C211A37B70025AFF2 /* Info.plist */, - ); - path = "Blockly WebView"; - sourceTree = ""; - }; - ABA1B7F9212214B9000D3CC5 /* Resources */ = { - isa = PBXGroup; - children = ( - ABA1B7FA212214C6000D3CC5 /* Non-Localized */, - ); - path = Resources; - sourceTree = ""; - }; - ABA1B7FA212214C6000D3CC5 /* Non-Localized */ = { - isa = PBXGroup; - children = ( - ABA1B7FB212214E7000D3CC5 /* Blockly */, - ); - path = "Non-Localized"; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - AB98010C211A37B50025AFF2 /* Blockly WebView */ = { - isa = PBXNativeTarget; - buildConfigurationList = AB98011F211A37B70025AFF2 /* Build configuration list for PBXNativeTarget "Blockly WebView" */; - buildPhases = ( - AB980109211A37B50025AFF2 /* Sources */, - AB98010A211A37B50025AFF2 /* Frameworks */, - ABEDABD1212372E700A66667 /* ShellScript */, - AB98010B211A37B50025AFF2 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = "Blockly WebView"; - productName = "Blockly WebView"; - productReference = AB98010D211A37B50025AFF2 /* Blockly WebView.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - AB980105211A37B50025AFF2 /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 0940; - LastUpgradeCheck = 0940; - ORGANIZATIONNAME = Google; - TargetAttributes = { - AB98010C211A37B50025AFF2 = { - CreatedOnToolsVersion = 9.4.1; - }; - }; - }; - buildConfigurationList = AB980108211A37B50025AFF2 /* Build configuration list for PBXProject "Blockly WebView" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = AB980104211A37B50025AFF2; - productRefGroup = AB98010E211A37B50025AFF2 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - AB98010C211A37B50025AFF2 /* Blockly WebView */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - AB98010B211A37B50025AFF2 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - AB98011B211A37B70025AFF2 /* LaunchScreen.storyboard in Resources */, - AB980118211A37B70025AFF2 /* Assets.xcassets in Resources */, - AB980116211A37B50025AFF2 /* Main.storyboard in Resources */, - ABA1B7FC212214E7000D3CC5 /* Blockly in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - ABEDABD1212372E700A66667 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = ./cp_resources.sh; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - AB980109211A37B50025AFF2 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - AB980113211A37B50025AFF2 /* ViewController.swift in Sources */, - AB980111211A37B50025AFF2 /* AppDelegate.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - AB980114211A37B50025AFF2 /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - AB980115211A37B50025AFF2 /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - AB980119211A37B70025AFF2 /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - AB98011A211A37B70025AFF2 /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - AB98011D211A37B70025AFF2 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.4; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - AB98011E211A37B70025AFF2 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.4; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - AB980120211A37B70025AFF2 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 3KZF7Q7Q49; - INFOPLIST_FILE = "Blockly WebView/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = "com.google.kidscoding.Blockly-WebView"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - AB980121211A37B70025AFF2 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 3KZF7Q7Q49; - INFOPLIST_FILE = "Blockly WebView/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = "com.google.kidscoding.Blockly-WebView"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - AB980108211A37B50025AFF2 /* Build configuration list for PBXProject "Blockly WebView" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - AB98011D211A37B70025AFF2 /* Debug */, - AB98011E211A37B70025AFF2 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - AB98011F211A37B70025AFF2 /* Build configuration list for PBXNativeTarget "Blockly WebView" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - AB980120211A37B70025AFF2 /* Debug */, - AB980121211A37B70025AFF2 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = AB980105211A37B50025AFF2 /* Project object */; -} diff --git a/demos/mobile/ios/Blockly WebView.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/demos/mobile/ios/Blockly WebView.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 2a4f4ae836a..00000000000 --- a/demos/mobile/ios/Blockly WebView.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/demos/mobile/ios/Blockly WebView.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/demos/mobile/ios/Blockly WebView.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003d6..00000000000 --- a/demos/mobile/ios/Blockly WebView.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/demos/mobile/ios/Blockly WebView.xcodeproj/xcuserdata/marshalla.xcuserdatad/xcschemes/xcschememanagement.plist b/demos/mobile/ios/Blockly WebView.xcodeproj/xcuserdata/marshalla.xcuserdatad/xcschemes/xcschememanagement.plist deleted file mode 100644 index e1e71ff066e..00000000000 --- a/demos/mobile/ios/Blockly WebView.xcodeproj/xcuserdata/marshalla.xcuserdatad/xcschemes/xcschememanagement.plist +++ /dev/null @@ -1,19 +0,0 @@ - - - - - SchemeUserState - - Blockly WebView.xcscheme - - orderHint - 0 - - Blockly WebView.xcscheme_^#shared#^_ - - orderHint - 0 - - - - diff --git a/demos/mobile/ios/Blockly WebView/AppDelegate.swift b/demos/mobile/ios/Blockly WebView/AppDelegate.swift deleted file mode 100644 index 1f2bf6eedce..00000000000 --- a/demos/mobile/ios/Blockly WebView/AppDelegate.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// AppDelegate.swift -// Blockly WebView -// -// Created by Andrew Marshall on 8/7/18. -// Copyright © 2018 Google. All rights reserved. -// - -import UIKit - -@UIApplicationMain -class AppDelegate: UIResponder, UIApplicationDelegate { - - var window: UIWindow? - - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. - return true - } - - func applicationWillResignActive(_ application: UIApplication) { - // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. - // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. - } - - func applicationDidEnterBackground(_ application: UIApplication) { - // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. - // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. - } - - func applicationWillEnterForeground(_ application: UIApplication) { - // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. - } - - func applicationDidBecomeActive(_ application: UIApplication) { - // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. - } - - func applicationWillTerminate(_ application: UIApplication) { - // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. - } - - -} - diff --git a/demos/mobile/ios/Blockly WebView/Assets.xcassets/AppIcon.appiconset/Contents.json b/demos/mobile/ios/Blockly WebView/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d8db8d65fd7..00000000000 --- a/demos/mobile/ios/Blockly WebView/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "images" : [ - { - "idiom" : "iphone", - "size" : "20x20", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "20x20", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "size" : "29x29", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "29x29", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "size" : "40x40", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "40x40", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "size" : "60x60", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "60x60", - "scale" : "3x" - }, - { - "idiom" : "ipad", - "size" : "20x20", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "20x20", - "scale" : "2x" - }, - { - "idiom" : "ipad", - "size" : "29x29", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "29x29", - "scale" : "2x" - }, - { - "idiom" : "ipad", - "size" : "40x40", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "40x40", - "scale" : "2x" - }, - { - "idiom" : "ipad", - "size" : "76x76", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "76x76", - "scale" : "2x" - }, - { - "idiom" : "ipad", - "size" : "83.5x83.5", - "scale" : "2x" - }, - { - "idiom" : "ios-marketing", - "size" : "1024x1024", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/demos/mobile/ios/Blockly WebView/Assets.xcassets/Contents.json b/demos/mobile/ios/Blockly WebView/Assets.xcassets/Contents.json deleted file mode 100644 index da4a164c918..00000000000 --- a/demos/mobile/ios/Blockly WebView/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/demos/mobile/ios/Blockly WebView/Base.lproj/LaunchScreen.storyboard b/demos/mobile/ios/Blockly WebView/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index f83f6fd5810..00000000000 --- a/demos/mobile/ios/Blockly WebView/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/demos/mobile/ios/Blockly WebView/Base.lproj/Main.storyboard b/demos/mobile/ios/Blockly WebView/Base.lproj/Main.storyboard deleted file mode 100644 index 35c8fdddfc2..00000000000 --- a/demos/mobile/ios/Blockly WebView/Base.lproj/Main.storyboard +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/demos/mobile/ios/Blockly WebView/Info.plist b/demos/mobile/ios/Blockly WebView/Info.plist deleted file mode 100644 index 16be3b68112..00000000000 --- a/demos/mobile/ios/Blockly WebView/Info.plist +++ /dev/null @@ -1,45 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIRequiredDeviceCapabilities - - armv7 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - - diff --git a/demos/mobile/ios/Blockly WebView/ViewController.swift b/demos/mobile/ios/Blockly WebView/ViewController.swift deleted file mode 100644 index e9f75dab465..00000000000 --- a/demos/mobile/ios/Blockly WebView/ViewController.swift +++ /dev/null @@ -1,110 +0,0 @@ -// ViewController.swift -// Blockly WebView UI controller. -// -// Created by Andrew Marshall on 8/7/18. -// Copyright © 2018 Google. All rights reserved. -// - -import UIKit -import WebKit - - -/// A basic ViewController for a WebView. -/// It handles the initial page load, and functions like window.prompt(). -class ViewController: UIViewController, WKUIDelegate { - /// The name used to reference this iOS object when executing callbacks from the JS code. - /// If this value is changed, it should also be changed in the `CODE_GENERATOR_BRIDGE_JS` file. - fileprivate static let HOST_HTML = "Blockly/webview.html" - - @IBOutlet weak var webView: WKWebView! - - /// Additional setup after loading the UI NIB. - override func viewDidLoad() { - super.viewDidLoad() - webView.uiDelegate = self - // Do any additional setup after loading the view, typically from a nib. - loadWebContent() - } - - /// Load the root HTML page into the webview. - func loadWebContent() { - if let htmlUrl = Bundle.main.url(forResource: "webview", withExtension: "html", - subdirectory: "Blockly") { - webView.load(URLRequest.init(url: htmlUrl)) - } else { - NSLog("Failed to load HTML. Could not find resource.") - } - } - - /// Handle window.alert() with a native dialog. - func webView(_ webView: WKWebView, - runJavaScriptAlertPanelWithMessage message: String, - initiatedByFrame frame: WKFrameInfo, - completionHandler: @escaping () -> Void) { - - let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert) - let title = NSLocalizedString("OK", comment: "OK Button") - let ok = UIAlertAction(title: title, style: .default) { (action: UIAlertAction) -> Void in - alert.dismiss(animated: true, completion: nil) - } - alert.addAction(ok) - present(alert, animated: true) - completionHandler() - } - - /// Handle window.confirm() with a native dialog. - func webView(_ webView: WKWebView, - runJavaScriptConfirmPanelWithMessage message: String, - initiatedByFrame frame: WKFrameInfo, - completionHandler: @escaping (Bool) -> Void) { - - let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert) - let closeAndHandle = { (okayed: Bool) in - alert.dismiss(animated: true, completion: nil) - completionHandler(okayed) - } - - let okTitle = NSLocalizedString("OK", comment: "OK button title") - let ok = UIAlertAction(title: okTitle, style: .default) { (action: UIAlertAction) -> Void in - closeAndHandle(true) - } - alert.addAction(ok) - - let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel button title") - let cancel = UIAlertAction(title: cancelTitle, style: .default) { - (action: UIAlertAction) -> Void in - closeAndHandle(false) - } - alert.addAction(cancel) - present(alert, animated: true) - } - - /// Handle window.prompt() with a native dialog. - func webView(_ webView: WKWebView, - runJavaScriptTextInputPanelWithPrompt prompt: String, - defaultText: String?, - initiatedByFrame frame: WKFrameInfo, - completionHandler: @escaping (String?) -> Void) { - - let alert = UIAlertController(title: prompt, message: nil, preferredStyle: .alert) - - alert.addTextField { (textField) in - textField.text = defaultText - } - - let okTitle = NSLocalizedString("OK", comment: "OK button title") - let okAction = UIAlertAction(title: okTitle, style: .default) { (_) in - let textInput = alert.textFields![0] as UITextField - completionHandler(textInput.text) - } - alert.addAction(okAction) - - let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel button title") - let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel) { (_) in - completionHandler(nil) - } - alert.addAction(cancelAction) - - present(alert, animated: true) - } -} diff --git a/demos/mobile/ios/cp_resources.sh b/demos/mobile/ios/cp_resources.sh deleted file mode 100755 index afb532bc5d9..00000000000 --- a/demos/mobile/ios/cp_resources.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash -set -eux - -BLOCKLY_ROOT=../../.. -IOS_RESOURCES=Resources/Non-Localized/Blockly - -MORE_FILES_TO_COPY=( - "dist/blockly_compressed.js" - "dist/blocks_compressed.js" - "media" - "build/msg" - ) - -mkdir -p $IOS_RESOURCES/media -mkdir -p $IOS_RESOURCES/msg/js -rsync -rp ../html/index.html $IOS_RESOURCES/webview.html -rsync -rp ../html/toolbox_standard.js $IOS_RESOURCES/toolbox_standard.js -for i in "${MORE_FILES_TO_COPY[@]}"; do # The quotes are necessary here - TARGET_DIR=$(dirname $IOS_RESOURCES/$i) - rsync -rp $BLOCKLY_ROOT/$i $TARGET_DIR -done diff --git a/gulpfile.js b/gulpfile.js deleted file mode 100644 index d2ad650c64a..00000000000 --- a/gulpfile.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * @license - * Copyright 2018 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Gulp script to build Blockly for Node & NPM. - * Run this script by calling "npm install" in this directory. - */ -/* eslint-env node */ - -const gulp = require('gulp'); - -const buildTasks = require('./scripts/gulpfiles/build_tasks'); -const packageTasks = require('./scripts/gulpfiles/package_tasks'); -const gitTasks = require('./scripts/gulpfiles/git_tasks'); -const appengineTasks = require('./scripts/gulpfiles/appengine_tasks'); -const releaseTasks = require('./scripts/gulpfiles/release_tasks'); -const docsTasks = require('./scripts/gulpfiles/docs_tasks'); -const testTasks = require('./scripts/gulpfiles/test_tasks'); - -module.exports = { - // Default target if gulp invoked without specifying. - default: buildTasks.build, - - // Main sequence targets. They already invoke prerequisites. - langfiles: buildTasks.langfiles, // Build build/msg/*.js from msg/json/*. - tsc: buildTasks.tsc, - deps: buildTasks.deps, - minify: buildTasks.minify, - build: buildTasks.build, - package: packageTasks.package, - publish: releaseTasks.publish, - publishBeta: releaseTasks.publishBeta, - prepareDemos: appengineTasks.prepareDemos, - deployDemos: appengineTasks.deployDemos, - deployDemosBeta: appengineTasks.deployDemosBeta, - gitUpdateGithubPages: gitTasks.updateGithubPages, - - // Manually-invokable targets, with prerequisites where required. - messages: buildTasks.messages, // Generate msg/json/en.json et al. - clean: gulp.parallel(buildTasks.cleanBuildDir, packageTasks.cleanReleaseDir), - test: testTasks.test, - testGenerators: testTasks.generators, - buildAdvancedCompilationTest: buildTasks.buildAdvancedCompilationTest, - gitCreateRC: gitTasks.createRC, - docs: docsTasks.docs, - - // Legacy targets, to be deleted. - recompile: releaseTasks.recompile, - gitSyncDevelop: gitTasks.syncDevelop, - gitSyncMaster: gitTasks.syncMaster, -}; diff --git a/media/click.ogg b/media/click.ogg deleted file mode 100644 index e8ae42a6106..00000000000 Binary files a/media/click.ogg and /dev/null differ diff --git a/media/click.wav b/media/click.wav deleted file mode 100644 index 41a50cd76f5..00000000000 Binary files a/media/click.wav and /dev/null differ diff --git a/media/delete.ogg b/media/delete.ogg deleted file mode 100644 index 67f84ac19a0..00000000000 Binary files a/media/delete.ogg and /dev/null differ diff --git a/media/delete.wav b/media/delete.wav deleted file mode 100644 index 18debcf96d6..00000000000 Binary files a/media/delete.wav and /dev/null differ diff --git a/media/disconnect.ogg b/media/disconnect.ogg deleted file mode 100644 index 467b527b4d0..00000000000 Binary files a/media/disconnect.ogg and /dev/null differ diff --git a/media/disconnect.wav b/media/disconnect.wav deleted file mode 100644 index af5c25447ca..00000000000 Binary files a/media/disconnect.wav and /dev/null differ diff --git a/package-lock.json b/package-lock.json index f23092e9b46..664b8c96903 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,36 +1,1504 @@ { - "name": "blockly", - "version": "11.2.2-mit-appinventor.1", + "name": "blockly-repo", + "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "blockly", - "version": "11.2.2-mit-appinventor.1", + "name": "blockly-repo", + "version": "0.0.0", + "license": "Apache-2.0", + "workspaces": [ + "packages/*" + ], + "devDependencies": { + "@commitlint/cli": "^20.1.0", + "@commitlint/config-conventional": "^20.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@commitlint/cli": { + "version": "20.3.1", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-20.3.1.tgz", + "integrity": "sha512-NtInjSlyev/+SLPvx/ulz8hRE25Wf5S9dLNDcIwazq0JyB4/w1ROF/5nV0ObPTX8YpRaKYeKtXDYWqumBNHWsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/format": "^20.3.1", + "@commitlint/lint": "^20.3.1", + "@commitlint/load": "^20.3.1", + "@commitlint/read": "^20.3.1", + "@commitlint/types": "^20.3.1", + "tinyexec": "^1.0.0", + "yargs": "^17.0.0" + }, + "bin": { + "commitlint": "cli.js" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/config-conventional": { + "version": "20.3.1", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-20.3.1.tgz", + "integrity": "sha512-NCzwvxepstBZbmVXsvg49s+shCxlJDJPWxXqONVcAtJH9wWrOlkMQw/zyl+dJmt8lyVopt5mwQ3mR5M2N2rUWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^20.3.1", + "conventional-changelog-conventionalcommits": "^7.0.2" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/config-validator": { + "version": "20.3.1", + "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-20.3.1.tgz", + "integrity": "sha512-ErVLC/IsHhcvxCyh+FXo7jy12/nkQySjWXYgCoQbZLkFp4hysov8KS6CdxBB0cWjbZWjvNOKBMNoUVqkmGmahw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^20.3.1", + "ajv": "^8.11.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/ensure": { + "version": "20.3.1", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-20.3.1.tgz", + "integrity": "sha512-h664FngOEd7bHAm0j8MEKq+qm2mH+V+hwJiIE2bWcw3pzJMlO0TPKtk0ATyRAtV6jQw+xviRYiIjjSjfajiB5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^20.3.1", + "lodash.camelcase": "^4.3.0", + "lodash.kebabcase": "^4.1.1", + "lodash.snakecase": "^4.1.1", + "lodash.startcase": "^4.4.0", + "lodash.upperfirst": "^4.3.1" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/execute-rule": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-20.0.0.tgz", + "integrity": "sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/format": { + "version": "20.3.1", + "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-20.3.1.tgz", + "integrity": "sha512-jfsjGPFTd2Yti2YHwUH4SPRPbWKAJAwrfa3eNa9bXEdrXBb9mCwbIrgYX38LdEJK9zLJ3AsLBP4/FLEtxyu2AA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^20.3.1", + "chalk": "^5.3.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/is-ignored": { + "version": "20.3.1", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-20.3.1.tgz", + "integrity": "sha512-tWwAoh93QvAhxgp99CzCuHD86MgxE4NBtloKX+XxQxhfhSwHo7eloiar/yzx53YW9eqSLP95zgW2KDDk4/WX+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^20.3.1", + "semver": "^7.6.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/lint": { + "version": "20.3.1", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-20.3.1.tgz", + "integrity": "sha512-LaOtrQ24+6SfUaWg8A+a+Wc77bvLbO5RIr6iy9F7CI3/0iq1uPEWgGRCwqWTuLGHkZDAcwaq0gZ01zpwZ1jCGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/is-ignored": "^20.3.1", + "@commitlint/parse": "^20.3.1", + "@commitlint/rules": "^20.3.1", + "@commitlint/types": "^20.3.1" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/load": { + "version": "20.3.1", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-20.3.1.tgz", + "integrity": "sha512-YDD9XA2XhgYgbjju8itZ/weIvOOobApDqwlPYCX5NLO/cPtw2UMO5Cmn44Ks8RQULUVI5fUT6roKvyxcoLbNmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/config-validator": "^20.3.1", + "@commitlint/execute-rule": "^20.0.0", + "@commitlint/resolve-extends": "^20.3.1", + "@commitlint/types": "^20.3.1", + "chalk": "^5.3.0", + "cosmiconfig": "^9.0.0", + "cosmiconfig-typescript-loader": "^6.1.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "lodash.uniq": "^4.5.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/message": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-20.0.0.tgz", + "integrity": "sha512-gLX4YmKnZqSwkmSB9OckQUrI5VyXEYiv3J5JKZRxIp8jOQsWjZgHSG/OgEfMQBK9ibdclEdAyIPYggwXoFGXjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/parse": { + "version": "20.3.1", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-20.3.1.tgz", + "integrity": "sha512-TuUTdbLpyUNLgDzLDYlI2BeTE6V/COZbf3f8WwsV0K6eq/2nSpNTMw7wHtXb+YxeY9wwxBp/Ldad4P+YIxHJoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^20.3.1", + "conventional-changelog-angular": "^7.0.0", + "conventional-commits-parser": "^5.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/read": { + "version": "20.3.1", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-20.3.1.tgz", + "integrity": "sha512-nCmJAdIg3OdNVUpQW0Idk/eF/vfOo2W2xzmvRmNeptLrzFK7qhwwl/kIwy1Q1LZrKHUFNj7PGNpIT5INbgZWzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/top-level": "^20.0.0", + "@commitlint/types": "^20.3.1", + "git-raw-commits": "^4.0.0", + "minimist": "^1.2.8", + "tinyexec": "^1.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/resolve-extends": { + "version": "20.3.1", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-20.3.1.tgz", + "integrity": "sha512-iGTGeyaoDyHDEZNjD8rKeosjSNs8zYanmuowY4ful7kFI0dnY4b5QilVYaFQJ6IM27S57LAeH5sKSsOHy4bw5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/config-validator": "^20.3.1", + "@commitlint/types": "^20.3.1", + "global-directory": "^4.0.1", + "import-meta-resolve": "^4.0.0", + "lodash.mergewith": "^4.6.2", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/rules": { + "version": "20.3.1", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-20.3.1.tgz", + "integrity": "sha512-/uic4P+4jVNpqQxz02+Y6vvIC0A2J899DBztA1j6q3f3MOKwydlNrojSh0dQmGDxxT1bXByiRtDhgFnOFnM6Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/ensure": "^20.3.1", + "@commitlint/message": "^20.0.0", + "@commitlint/to-lines": "^20.0.0", + "@commitlint/types": "^20.3.1" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/to-lines": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-20.0.0.tgz", + "integrity": "sha512-2l9gmwiCRqZNWgV+pX1X7z4yP0b3ex/86UmUFgoRt672Ez6cAM2lOQeHFRUTuE6sPpi8XBCGnd8Kh3bMoyHwJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/top-level": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-20.0.0.tgz", + "integrity": "sha512-drXaPSP2EcopukrUXvUXmsQMu3Ey/FuJDc/5oiW4heoCfoE5BdLQyuc7veGeE3aoQaTVqZnh4D5WTWe2vefYKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^7.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/types": { + "version": "20.3.1", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-20.3.1.tgz", + "integrity": "sha512-VmIFV/JkBRhDRRv7N5B7zEUkNZIx9Mp+8Pe65erz0rKycXLsi8Epcw0XJ+btSeRXgTzE7DyOyA9bkJ9mn/yqVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/conventional-commits-parser": "^5.0.0", + "chalk": "^5.3.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@types/conventional-commits-parser": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.2.tgz", + "integrity": "sha512-BgT2szDXnVypgpNxOK8aL5SGjUdaQbC++WZNjF1Qge3Og2+zhHj+RWhmehLhYyvQwqAmvezruVfOf8+3m74W+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "25.0.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.9.tgz", + "integrity": "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-ify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", + "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", + "dev": true, + "license": "MIT" + }, + "node_modules/b4a": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", + "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/blockly": { + "resolved": "packages/blockly", + "link": true + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/compare-func": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", + "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-ify": "^1.0.0", + "dot-prop": "^5.1.0" + } + }, + "node_modules/conventional-changelog-angular": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-7.0.0.tgz", + "integrity": "sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/conventional-changelog-conventionalcommits": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-7.0.2.tgz", + "integrity": "sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/conventional-commits-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", + "integrity": "sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-text-path": "^2.0.0", + "JSONStream": "^1.3.5", + "meow": "^12.0.1", + "split2": "^4.0.0" + }, + "bin": { + "conventional-commits-parser": "cli.mjs" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cosmiconfig-typescript-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-6.2.0.tgz", + "integrity": "sha512-GEN39v7TgdxgIoNcdkRE3uiAzQt3UXLyHbRHD6YoL048XAeOomyxaP+Hh/+2C6C2wYjxJ2onhJcsQp+L4YEkVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "jiti": "^2.6.1" + }, + "engines": { + "node": ">=v18" + }, + "peerDependencies": { + "@types/node": "*", + "cosmiconfig": ">=9", + "typescript": ">=5" + } + }, + "node_modules/dargs": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-8.1.0.tgz", + "integrity": "sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/find-up": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-7.0.0.tgz", + "integrity": "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^7.2.0", + "path-exists": "^5.0.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/git-raw-commits": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-4.0.0.tgz", + "integrity": "sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dargs": "^8.0.0", + "meow": "^12.0.1", + "split2": "^4.0.0" + }, + "bin": { + "git-raw-commits": "cli.mjs" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/global-directory": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", + "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "4.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/google-closure-compiler": { + "version": "20260114.0.0", + "resolved": "https://registry.npmjs.org/google-closure-compiler/-/google-closure-compiler-20260114.0.0.tgz", + "integrity": "sha512-6zOmAfIkEuj2D+52q1TGTzGypYuxra5uc7YOij3GTLQRMoT6gcJ+hnMM1lWVxibksvISe2ieLBjOjHKyVp3Fkw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "chalk": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 <5.6.1 || ^5.6.2 >5.6.1", + "google-closure-compiler-java": "^20260114.0.0", + "minimist": "^1.0.0", + "vinyl": "^3.0.1", + "vinyl-sourcemaps-apply": "^0.2.0" + }, + "bin": { + "google-closure-compiler": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "google-closure-compiler-linux": "^20260114.0.0", + "google-closure-compiler-linux-arm64": "^20260114.0.0", + "google-closure-compiler-macos": "^20260114.0.0", + "google-closure-compiler-windows": "^20260114.0.0" + } + }, + "node_modules/google-closure-compiler-java": { + "version": "20260114.0.0", + "resolved": "https://registry.npmjs.org/google-closure-compiler-java/-/google-closure-compiler-java-20260114.0.0.tgz", + "integrity": "sha512-JBv3hVie4mI/DFtrrl6eMm0yHY/sp6wnKd6VIOL/eAxWDSHN7NUxnrmT7bwZpsKQU+Z0frPeBoJtIXiZZJxkjA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/google-closure-compiler-linux": { + "version": "20260114.0.0", + "resolved": "https://registry.npmjs.org/google-closure-compiler-linux/-/google-closure-compiler-linux-20260114.0.0.tgz", + "integrity": "sha512-u8N6mGsUiHhNrVGyGTewWT2GyqTuiSZzq2T/Kyxc8cA06eQaprvx4ljkeH5eVRispItJH7uPwbxCjZ/zdNRBuA==", + "cpu": [ + "x32", + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/google-closure-compiler-linux-arm64": { + "version": "20260114.0.0", + "resolved": "https://registry.npmjs.org/google-closure-compiler-linux-arm64/-/google-closure-compiler-linux-arm64-20260114.0.0.tgz", + "integrity": "sha512-qArfqTXVFPbjh6sSWuObo7cTTnpFVkqyChdTFoEKcySmGMiOoJjgyk6BeH6+wRpum9C/6Hi9jhN3yaZ7g1PZmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/google-closure-compiler-macos": { + "version": "20260114.0.0", + "resolved": "https://registry.npmjs.org/google-closure-compiler-macos/-/google-closure-compiler-macos-20260114.0.0.tgz", + "integrity": "sha512-hlqaZqSvMp8Se/LeJaUW+RjSIlXxG8MvA58f0EpKw6DsFAmzLBa4QvFYW+t1BFmiMxR8LIX3wkijzzjC8i43ZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/google-closure-compiler-windows": { + "version": "20260114.0.0", + "resolved": "https://registry.npmjs.org/google-closure-compiler-windows/-/google-closure-compiler-windows-20260114.0.0.tgz", + "integrity": "sha512-LtEPcrhe7ls9Mqlk6jRdoPlHM2eo1k+CJ5MaOrN8wBCi6XWcNdjeS1OFXAHgN4/tcNjg4qNsLHsHUk+bJ70hZQ==", + "cpu": [ + "x32", + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-text-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-2.0.0.tgz", + "integrity": "sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "text-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + }, + "bin": { + "JSONStream": "bin.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.startcase": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", + "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.upperfirst": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", + "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/meow": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", + "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "dev": true, + "license": "ISC" + }, + "node_modules/replace-ext": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/text-extensions": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz", + "integrity": "sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vinyl": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.1.tgz", + "integrity": "sha512-0QwqXteBNXgnLCdWdvPQBX6FXRHtIH3VhJPTd5Lwn28tJXc34YqSCWUmkOvtJHBmB3gGoPtrOKk3Ts8/kEZ9aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^2.1.2", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-sourcemaps-apply": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz", + "integrity": "sha512-+oDh3KYZBoZC8hfocrbrxbLUeaYtQK7J5WU5Br9VqWqmCll3tFJqKp97GC9GmMsVIL0qnx2DgEDVxdo5EZ5sSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "source-map": "^0.5.1" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/blockly": { + "version": "12.4.1-mit-appinventor.1", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "jsdom": "25.0.1" + "jsdom": "26.1.0" }, "devDependencies": { - "@blockly/block-test": "^6.0.4", - "@blockly/dev-tools": "^8.0.6", - "@blockly/theme-modern": "^6.0.3", + "@blockly/block-test": "^7.0.2", + "@blockly/dev-tools": "^9.0.2", + "@blockly/keyboard-navigation": "^3.0.1", + "@blockly/theme-modern": "^7.0.1", "@hyperjump/browser": "^1.1.4", "@hyperjump/json-schema": "^1.5.0", - "@microsoft/api-documenter": "^7.22.4", + "@microsoft/api-documenter": "7.22.4", "@microsoft/api-extractor": "^7.29.5", + "ajv": "^8.17.1", "async-done": "^2.0.0", - "chai": "^5.1.1", + "chai": "^6.0.1", "concurrently": "^9.0.1", "eslint": "^9.15.0", "eslint-config-google": "^0.14.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-jsdoc": "^50.5.0", + "eslint-config-prettier": "^10.1.1", + "eslint-plugin-jsdoc": "^52.0.2", + "eslint-plugin-mocha": "^11.1.0", "eslint-plugin-prettier": "^5.2.1", "glob": "^11.0.1", - "globals": "^15.12.0", - "google-closure-compiler": "^20240317.0.0", + "globals": "^16.0.0", + "google-closure-compiler": "^20260114.0.0", "gulp": "^5.0.0", "gulp-concat": "^2.6.1", "gulp-gzip": "^1.4.2", @@ -45,10 +1513,11 @@ "http-server": "^14.0.0", "json5": "^2.2.0", "markdown-tables-to-json": "^0.1.7", - "mocha": "^10.0.0", + "mocha": "^11.3.0", "patch-package": "^8.0.0", "prettier": "^3.3.3", "prettier-plugin-organize-imports": "^4.0.0", + "puppeteer-core": "^24.17.0", "readline-sync": "^1.4.10", "rimraf": "^5.0.0", "typescript": "^5.3.3", @@ -60,38 +1529,50 @@ "node": ">=18" } }, - "node_modules/@aashutoshrathi/word-wrap": { + "packages/blockly/node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/@blockly/block-test": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/@blockly/block-test/-/block-test-6.0.11.tgz", - "integrity": "sha512-aIgcxkof1gLJtJXKSvmnug9iSXbv5Qilnov4Sa/QNURiWJRxvMNqWiTZJVu/reuCQK4Qm4jadg9R9l+eu7ujvw==", + "packages/blockly/node_modules/@asamuzakjp/css-color": { + "version": "3.1.1", + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.2", + "@csstools/css-color-parser": "^3.0.8", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "packages/blockly/node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "license": "ISC" + }, + "packages/blockly/node_modules/@blockly/block-test": { + "version": "7.0.2", "dev": true, + "license": "Apache 2.0", "engines": { "node": ">=8.17.0" }, "peerDependencies": { - "blockly": "^11.0.0" + "blockly": "^12.0.0" } }, - "node_modules/@blockly/dev-tools": { - "version": "8.0.12", - "resolved": "https://registry.npmjs.org/@blockly/dev-tools/-/dev-tools-8.0.12.tgz", - "integrity": "sha512-jE0y/Z7ggmM2JS4l0Xf2ic3eecuM+ZDjUZNCcM2k6yy0VDJoxOPN63Cq2soswXQRuKHfzRMHY48rCvoKL3MqPA==", + "packages/blockly/node_modules/@blockly/dev-tools": { + "version": "9.0.2", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@blockly/block-test": "^6.0.11", - "@blockly/theme-dark": "^7.0.10", - "@blockly/theme-deuteranopia": "^6.0.10", - "@blockly/theme-highcontrast": "^6.0.10", - "@blockly/theme-tritanopia": "^6.0.10", + "@blockly/block-test": "^7.0.2", + "@blockly/theme-dark": "^8.0.1", + "@blockly/theme-deuteranopia": "^7.0.1", + "@blockly/theme-highcontrast": "^7.0.1", + "@blockly/theme-tritanopia": "^7.0.1", "chai": "^4.2.0", "dat.gui": "^0.7.7", "lodash.assign": "^4.2.0", @@ -103,23 +1584,21 @@ "node": ">=8.0.0" }, "peerDependencies": { - "blockly": "^11.0.0" + "blockly": "^12.0.0" } }, - "node_modules/@blockly/dev-tools/node_modules/assertion-error": { + "packages/blockly/node_modules/@blockly/dev-tools/node_modules/assertion-error": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", "dev": true, + "license": "MIT", "engines": { "node": "*" } }, - "node_modules/@blockly/dev-tools/node_modules/chai": { + "packages/blockly/node_modules/@blockly/dev-tools/node_modules/chai": { "version": "4.4.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", - "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", "dev": true, + "license": "MIT", "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", @@ -133,11 +1612,10 @@ "node": ">=4" } }, - "node_modules/@blockly/dev-tools/node_modules/check-error": { + "packages/blockly/node_modules/@blockly/dev-tools/node_modules/check-error": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", "dev": true, + "license": "MIT", "dependencies": { "get-func-name": "^2.0.2" }, @@ -145,11 +1623,10 @@ "node": "*" } }, - "node_modules/@blockly/dev-tools/node_modules/deep-eql": { + "packages/blockly/node_modules/@blockly/dev-tools/node_modules/deep-eql": { "version": "4.1.4", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", - "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", "dev": true, + "license": "MIT", "dependencies": { "type-detect": "^4.0.0" }, @@ -157,129 +1634,231 @@ "node": ">=6" } }, - "node_modules/@blockly/dev-tools/node_modules/loupe": { + "packages/blockly/node_modules/@blockly/dev-tools/node_modules/loupe": { "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", "dev": true, + "license": "MIT", "dependencies": { "get-func-name": "^2.0.1" } }, - "node_modules/@blockly/dev-tools/node_modules/pathval": { + "packages/blockly/node_modules/@blockly/dev-tools/node_modules/pathval": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "packages/blockly/node_modules/@blockly/keyboard-navigation": { + "version": "3.0.1", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "blockly": "^12.3.0" + } + }, + "packages/blockly/node_modules/@blockly/theme-dark": { + "version": "8.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.17.0" + }, + "peerDependencies": { + "blockly": "^12.0.0" + } + }, + "packages/blockly/node_modules/@blockly/theme-deuteranopia": { + "version": "7.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.17.0" + }, + "peerDependencies": { + "blockly": "^12.0.0" + } + }, + "packages/blockly/node_modules/@blockly/theme-highcontrast": { + "version": "7.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.17.0" + }, + "peerDependencies": { + "blockly": "^12.0.0" + } + }, + "packages/blockly/node_modules/@blockly/theme-modern": { + "version": "7.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.17.0" + }, + "peerDependencies": { + "blockly": "^12.0.0" + } + }, + "packages/blockly/node_modules/@blockly/theme-tritanopia": { + "version": "7.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.17.0" + }, + "peerDependencies": { + "blockly": "^12.0.0" + } + }, + "packages/blockly/node_modules/@csstools/color-helpers": { + "version": "5.0.2", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "engines": { - "node": "*" + "node": ">=18" } }, - "node_modules/@blockly/theme-dark": { - "version": "7.0.10", - "resolved": "https://registry.npmjs.org/@blockly/theme-dark/-/theme-dark-7.0.10.tgz", - "integrity": "sha512-Wc6n115vt9alxzPkEwYtvBBGoPUV3gaYE00dvSKhqXTNoy1Xioujj9kT9VkGmdMO2mhgnJNczSpvxG8tcd4zLQ==", - "dev": true, + "packages/blockly/node_modules/@csstools/css-calc": { + "version": "2.1.2", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", "engines": { - "node": ">=8.17.0" + "node": ">=18" }, "peerDependencies": { - "blockly": "^11.0.0" + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" } }, - "node_modules/@blockly/theme-deuteranopia": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/@blockly/theme-deuteranopia/-/theme-deuteranopia-6.0.10.tgz", - "integrity": "sha512-im5nIvf/Z0f1vJ9DK5Euu6URfY8G44xeFsat2b7TySF0BfAUWkGsagK3C6D5NatigPxKZqz3exC9zeXEtprAcg==", - "dev": true, - "engines": { - "node": ">=8.17.0" + "packages/blockly/node_modules/@csstools/css-color-parser": { + "version": "3.0.8", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.0.2", + "@csstools/css-calc": "^2.1.2" }, - "peerDependencies": { - "blockly": "^11.0.0" - } - }, - "node_modules/@blockly/theme-highcontrast": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/@blockly/theme-highcontrast/-/theme-highcontrast-6.0.10.tgz", - "integrity": "sha512-s1hehl/b50IhebCs20hm2hFWbUTqJ2YSGdR0gnp2NLfNNRWwyZHZk+q4aG3k4L0YBWjNfE3XiRCkDISy83dBIA==", - "dev": true, "engines": { - "node": ">=8.17.0" + "node": ">=18" }, "peerDependencies": { - "blockly": "^11.0.0" + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" } }, - "node_modules/@blockly/theme-modern": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/@blockly/theme-modern/-/theme-modern-6.0.10.tgz", - "integrity": "sha512-xOVf5Vq5ACgbVsaNAKWb5cE0msUfBxj1G1asp0aBmWo1QCr3Yze4rUtFDaNIoeCd8EsRpuWZgBYg74zPL9eAow==", - "dev": true, + "packages/blockly/node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.4", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", "engines": { - "node": ">=8.17.0" + "node": ">=18" }, "peerDependencies": { - "blockly": "^11.0.0" + "@csstools/css-tokenizer": "^3.0.3" } }, - "node_modules/@blockly/theme-tritanopia": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/@blockly/theme-tritanopia/-/theme-tritanopia-6.0.10.tgz", - "integrity": "sha512-QNIvUHokGMLnCWUzERRZa6sSkD5RIUynWDI+KNurBH21NeWnSNScQiNu0dS/w5MSkZ/Iqqbi79UZoF49SzEayg==", - "dev": true, + "packages/blockly/node_modules/@csstools/css-tokenizer": { + "version": "3.0.3", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", "engines": { - "node": ">=8.17.0" - }, - "peerDependencies": { - "blockly": "^11.0.0" + "node": ">=18" } }, - "node_modules/@es-joy/jsdoccomment": { - "version": "0.49.0", - "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.49.0.tgz", - "integrity": "sha512-xjZTSFgECpb9Ohuk5yMX5RhUEbfeQcuOp8IF60e+wyzWEF0M5xeSgqsfLtvPEX8BIyOX9saZqzuGPmZ8oWc+5Q==", + "packages/blockly/node_modules/@es-joy/jsdoccomment": { + "version": "0.52.0", "dev": true, + "license": "MIT", "dependencies": { + "@types/estree": "^1.0.8", + "@typescript-eslint/types": "^8.34.1", "comment-parser": "1.4.1", "esquery": "^1.6.0", "jsdoc-type-pratt-parser": "~4.1.0" }, "engines": { - "node": ">=16" + "node": ">=20.11.0" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "packages/blockly/node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", "dev": true, + "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@eslint-community/regexpp": { + "packages/blockly/node_modules/@eslint-community/regexpp": { "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@eslint/config-array": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.0.tgz", - "integrity": "sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ==", + "packages/blockly/node_modules/@eslint/config-array": { + "version": "0.21.0", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.4", + "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -287,20 +1866,29 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/core": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.0.tgz", - "integrity": "sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg==", + "packages/blockly/node_modules/@eslint/config-helpers": { + "version": "0.3.1", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/eslintrc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", - "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", + "packages/blockly/node_modules/@eslint/core": { + "version": "0.15.2", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "packages/blockly/node_modules/@eslint/eslintrc": { + "version": "3.3.1", "dev": true, + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -319,11 +1907,25 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/globals": { + "packages/blockly/node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "packages/blockly/node_modules/@eslint/eslintrc/node_modules/globals": { "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -331,41 +1933,46 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@eslint/js": { - "version": "9.17.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz", - "integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==", + "packages/blockly/node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "packages/blockly/node_modules/@eslint/js": { + "version": "9.36.0", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, - "node_modules/@eslint/object-schema": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", - "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "packages/blockly/node_modules/@eslint/object-schema": { + "version": "2.1.6", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/plugin-kit": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz", - "integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==", + "packages/blockly/node_modules/@eslint/plugin-kit": { + "version": "0.3.5", "dev": true, + "license": "Apache-2.0", "dependencies": { + "@eslint/core": "^0.15.2", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@gulp-sourcemaps/identity-map": { + "packages/blockly/node_modules/@gulp-sourcemaps/identity-map": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/identity-map/-/identity-map-2.0.1.tgz", - "integrity": "sha512-Tb+nSISZku+eQ4X1lAkevcQa+jknn/OVUgZ3XCxEKIsLsqYuPoJwJOPQeaOk75X3WPftb29GWY1eqE7GLsXb1Q==", "dev": true, + "license": "MIT", "dependencies": { "acorn": "^6.4.1", "normalize-path": "^3.0.0", @@ -377,11 +1984,10 @@ "node": ">= 0.10" } }, - "node_modules/@gulp-sourcemaps/identity-map/node_modules/acorn": { + "packages/blockly/node_modules/@gulp-sourcemaps/identity-map/node_modules/acorn": { "version": "6.4.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", - "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -389,30 +1995,27 @@ "node": ">=0.4.0" } }, - "node_modules/@gulp-sourcemaps/identity-map/node_modules/source-map": { + "packages/blockly/node_modules/@gulp-sourcemaps/identity-map/node_modules/source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, - "node_modules/@gulp-sourcemaps/identity-map/node_modules/through2": { + "packages/blockly/node_modules/@gulp-sourcemaps/identity-map/node_modules/through2": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", - "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", "dev": true, + "license": "MIT", "dependencies": { "inherits": "^2.0.4", "readable-stream": "2 || 3" } }, - "node_modules/@gulp-sourcemaps/map-sources": { + "packages/blockly/node_modules/@gulp-sourcemaps/map-sources": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/map-sources/-/map-sources-1.0.0.tgz", - "integrity": "sha1-iQrnxdjId/bThIYCFazp1+yUW9o=", "dev": true, + "license": "MIT", "dependencies": { "normalize-path": "^2.0.1", "through2": "^2.0.3" @@ -421,11 +2024,10 @@ "node": ">= 0.10" } }, - "node_modules/@gulp-sourcemaps/map-sources/node_modules/normalize-path": { + "packages/blockly/node_modules/@gulp-sourcemaps/map-sources/node_modules/normalize-path": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", "dev": true, + "license": "MIT", "dependencies": { "remove-trailing-separator": "^1.0.1" }, @@ -433,11 +2035,10 @@ "node": ">=0.10.0" } }, - "node_modules/@gulp-sourcemaps/map-sources/node_modules/readable-stream": { + "packages/blockly/node_modules/@gulp-sourcemaps/map-sources/node_modules/readable-stream": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, + "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -448,30 +2049,27 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/@gulp-sourcemaps/map-sources/node_modules/through2": { + "packages/blockly/node_modules/@gulp-sourcemaps/map-sources/node_modules/through2": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", "dev": true, + "license": "MIT", "dependencies": { "readable-stream": "~2.3.6", "xtend": "~4.0.1" } }, - "node_modules/@gulpjs/messages": { + "packages/blockly/node_modules/@gulpjs/messages": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@gulpjs/messages/-/messages-1.1.0.tgz", - "integrity": "sha512-Ys9sazDatyTgZVb4xPlDufLweJ/Os2uHWOv+Caxvy2O85JcnT4M3vc73bi8pdLWlv3fdWQz3pdI9tVwo8rQQSg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.13.0" } }, - "node_modules/@gulpjs/to-absolute-glob": { + "packages/blockly/node_modules/@gulpjs/to-absolute-glob": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@gulpjs/to-absolute-glob/-/to-absolute-glob-4.0.0.tgz", - "integrity": "sha512-kjotm7XJrJ6v+7knhPaRgaT6q8F8K2jiafwYdNHLzmV0uGLuZY43FK6smNSHUPrhq5kX2slCUy+RGG/xGqmIKA==", "dev": true, + "license": "MIT", "dependencies": { "is-negated-glob": "^1.0.0" }, @@ -479,20 +2077,18 @@ "node": ">=10.13.0" } }, - "node_modules/@humanfs/core": { + "packages/blockly/node_modules/@humanfs/core": { "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=18.18.0" } }, - "node_modules/@humanfs/node": { + "packages/blockly/node_modules/@humanfs/node": { "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" @@ -501,11 +2097,10 @@ "node": ">=18.18.0" } }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "packages/blockly/node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=18.18" }, @@ -514,11 +2109,10 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/module-importer": { + "packages/blockly/node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=12.22" }, @@ -527,11 +2121,10 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", - "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "packages/blockly/node_modules/@humanwhocodes/retry": { + "version": "0.4.2", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=18.18" }, @@ -540,11 +2133,10 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@hyperjump/browser": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@hyperjump/browser/-/browser-1.1.6.tgz", - "integrity": "sha512-i27uPV7SxK1GOn7TLTRxTorxchYa5ur9JHgtl6TxZ1MHuyb9ROAnXxEeu4q4H1836Xb7lL2PGPsaa5Jl3p+R6g==", + "packages/blockly/node_modules/@hyperjump/browser": { + "version": "1.3.1", "dev": true, + "license": "MIT", "dependencies": { "@hyperjump/json-pointer": "^1.1.0", "@hyperjump/uri": "^1.2.0", @@ -559,20 +2151,17 @@ "url": "https://github.com/sponsors/jdesrosiers" } }, - "node_modules/@hyperjump/json-pointer": { + "packages/blockly/node_modules/@hyperjump/json-pointer": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@hyperjump/json-pointer/-/json-pointer-1.1.0.tgz", - "integrity": "sha512-tFCKxMKDKK3VEdtUA3EBOS9GmSOS4mbrTjh9v3RnK10BphDMOb6+bxTh++/ae1AyfHyWb6R54O/iaoAtPMZPCg==", "dev": true, + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/jdesrosiers" } }, - "node_modules/@hyperjump/json-schema": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@hyperjump/json-schema/-/json-schema-1.11.0.tgz", - "integrity": "sha512-gX1YNObOybUW6tgJjvb1lomNbI/VnY+EBPokmEGy9Lk8cgi+gE0vXhX1XDgIpUUA4UXfgHEn5I1mga5vHgOttg==", + "packages/blockly/node_modules/@hyperjump/json-schema": { + "version": "1.15.1", "dev": true, "license": "MIT", "dependencies": { @@ -592,11 +2181,10 @@ "@hyperjump/browser": "^1.1.0" } }, - "node_modules/@hyperjump/pact": { + "packages/blockly/node_modules/@hyperjump/pact": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@hyperjump/pact/-/pact-1.2.0.tgz", - "integrity": "sha512-+NirBesJkhgZMRXzza8flnh0wwIuHZ9wSYjXSNAA1KQjHtn4Nho1wi3Y5PC7izBvoPKrPFt7J+qtEUkosav+zQ==", "dev": true, + "license": "MIT", "dependencies": { "just-curry-it": "^5.3.0" }, @@ -605,20 +2193,36 @@ "url": "https://github.com/sponsors/jdesrosiers" } }, - "node_modules/@hyperjump/uri": { + "packages/blockly/node_modules/@hyperjump/uri": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@hyperjump/uri/-/uri-1.2.0.tgz", - "integrity": "sha512-v/OE8Kg0xdd1wYRjyAI8zPxQEAgWuhqSy5mJm0/FAIUdN6S6b75DBUSl2J3ps6QSCID3fnjXqJyevrOOH67YAA==", "dev": true, + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/jdesrosiers" } }, - "node_modules/@isaacs/cliui": { + "packages/blockly/node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "packages/blockly/node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "packages/blockly/node_modules/@isaacs/cliui": { "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, "license": "ISC", "dependencies": { @@ -633,10 +2237,8 @@ "node": ">=12" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "packages/blockly/node_modules/@isaacs/cliui/node_modules/ansi-styles": { "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, "license": "MIT", "engines": { @@ -646,17 +2248,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "packages/blockly/node_modules/@isaacs/cliui/node_modules/emoji-regex": { "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true, "license": "MIT" }, - "node_modules/@isaacs/cliui/node_modules/string-width": { + "packages/blockly/node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, "license": "MIT", "dependencies": { @@ -671,10 +2269,8 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "packages/blockly/node_modules/@isaacs/cliui/node_modules/strip-ansi": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, "license": "MIT", "dependencies": { @@ -687,10 +2283,8 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "packages/blockly/node_modules/@isaacs/cliui/node_modules/wrap-ansi": { "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, "license": "MIT", "dependencies": { @@ -705,18 +2299,16 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/@microsoft/api-documenter": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@microsoft/api-documenter/-/api-documenter-7.26.7.tgz", - "integrity": "sha512-VruYlHYAhQfuBNyndvyD9GBmCRWSOZ8D9+eXicygycNPMC/SPM71WWu2OwP98CTBvq/OhG/uRvUGGdL4QvgiFQ==", + "packages/blockly/node_modules/@microsoft/api-documenter": { + "version": "7.22.4", "dev": true, "license": "MIT", "dependencies": { - "@microsoft/api-extractor-model": "7.30.3", - "@microsoft/tsdoc": "~0.15.1", - "@rushstack/node-core-library": "5.11.0", - "@rushstack/terminal": "0.14.6", - "@rushstack/ts-command-line": "4.23.4", + "@microsoft/api-extractor-model": "7.26.8", + "@microsoft/tsdoc": "0.14.2", + "@rushstack/node-core-library": "3.58.0", + "@rushstack/ts-command-line": "4.13.2", + "colors": "~1.2.1", "js-yaml": "~3.13.1", "resolve": "~1.22.1" }, @@ -724,107 +2316,56 @@ "api-documenter": "bin/api-documenter" } }, - "node_modules/@microsoft/api-documenter/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/@microsoft/api-documenter/node_modules/js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "packages/blockly/node_modules/@microsoft/api-documenter/node_modules/@microsoft/api-extractor-model": { + "version": "7.26.8", "dev": true, + "license": "MIT", "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "~0.16.1", + "@rushstack/node-core-library": "3.58.0" } }, - "node_modules/@microsoft/api-extractor": { - "version": "7.48.1", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.48.1.tgz", - "integrity": "sha512-HN9Osa1WxqLM66RaqB5nPAadx+nTIQmY/XtkFdaJvusjG8Tus++QqZtD7KPZDSkhEMGHsYeSyeU8qUzCDUXPjg==", + "packages/blockly/node_modules/@microsoft/api-documenter/node_modules/@microsoft/tsdoc": { + "version": "0.14.2", "dev": true, - "dependencies": { - "@microsoft/api-extractor-model": "7.30.1", - "@microsoft/tsdoc": "~0.15.1", - "@microsoft/tsdoc-config": "~0.17.1", - "@rushstack/node-core-library": "5.10.1", - "@rushstack/rig-package": "0.5.3", - "@rushstack/terminal": "0.14.4", - "@rushstack/ts-command-line": "4.23.2", - "lodash": "~4.17.15", - "minimatch": "~3.0.3", - "resolve": "~1.22.1", - "semver": "~7.5.4", - "source-map": "~0.6.1", - "typescript": "5.4.2" - }, - "bin": { - "api-extractor": "bin/api-extractor" - } + "license": "MIT" }, - "node_modules/@microsoft/api-extractor-model": { - "version": "7.30.3", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.30.3.tgz", - "integrity": "sha512-yEAvq0F78MmStXdqz9TTT4PZ05Xu5R8nqgwI5xmUmQjWBQ9E6R2n8HB/iZMRciG4rf9iwI2mtuQwIzDXBvHn1w==", + "packages/blockly/node_modules/@microsoft/api-documenter/node_modules/@microsoft/tsdoc-config": { + "version": "0.16.2", "dev": true, "license": "MIT", "dependencies": { - "@microsoft/tsdoc": "~0.15.1", - "@microsoft/tsdoc-config": "~0.17.1", - "@rushstack/node-core-library": "5.11.0" + "@microsoft/tsdoc": "0.14.2", + "ajv": "~6.12.6", + "jju": "~1.4.0", + "resolve": "~1.19.0" } }, - "node_modules/@microsoft/api-extractor/node_modules/@microsoft/api-extractor-model": { - "version": "7.30.1", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.30.1.tgz", - "integrity": "sha512-CTS2PlASJHxVY8hqHORVb1HdECWOEMcMnM6/kDkPr0RZapAFSIHhg9D4jxuE8g+OWYHtPc10LCpmde5pylTRlA==", + "packages/blockly/node_modules/@microsoft/api-documenter/node_modules/@microsoft/tsdoc-config/node_modules/resolve": { + "version": "1.19.0", "dev": true, + "license": "MIT", "dependencies": { - "@microsoft/tsdoc": "~0.15.1", - "@microsoft/tsdoc-config": "~0.17.1", - "@rushstack/node-core-library": "5.10.1" + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@microsoft/api-extractor/node_modules/@rushstack/node-core-library": { - "version": "5.10.1", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.10.1.tgz", - "integrity": "sha512-BSb/KcyBHmUQwINrgtzo6jiH0HlGFmrUy33vO6unmceuVKTEyL2q+P0fQq2oB5hvXVWOEUhxB2QvlkZluvUEmg==", + "packages/blockly/node_modules/@microsoft/api-documenter/node_modules/@rushstack/node-core-library": { + "version": "3.58.0", "dev": true, + "license": "MIT", "dependencies": { - "ajv": "~8.13.0", - "ajv-draft-04": "~1.0.0", - "ajv-formats": "~3.0.1", + "colors": "~1.2.1", "fs-extra": "~7.0.1", "import-lazy": "~4.0.0", "jju": "~1.4.0", "resolve": "~1.22.1", - "semver": "~7.5.4" - }, - "peerDependencies": { - "@types/node": "*" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@microsoft/api-extractor/node_modules/@rushstack/terminal": { - "version": "0.14.4", - "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.14.4.tgz", - "integrity": "sha512-NxACqERW0PHq8Rpq1V6v5iTHEwkRGxenjEW+VWqRYQ8T9puUzgmGHmEZUaUEDHAe9Qyvp0/Ew04sAiQw9XjhJg==", - "dev": true, - "dependencies": { - "@rushstack/node-core-library": "5.10.1", - "supports-color": "~8.1.1" + "semver": "~7.3.0", + "z-schema": "~5.0.2" }, "peerDependencies": { "@types/node": "*" @@ -835,90 +2376,130 @@ } } }, - "node_modules/@microsoft/api-extractor/node_modules/@rushstack/ts-command-line": { - "version": "4.23.2", - "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.23.2.tgz", - "integrity": "sha512-JJ7XZX5K3ThBBva38aomgsPv1L7FV6XmSOcR6HtM7HDFZJkepqT65imw26h9ggGqMjsY0R9jcl30tzKcVj9aOQ==", + "packages/blockly/node_modules/@microsoft/api-documenter/node_modules/@rushstack/ts-command-line": { + "version": "4.13.2", "dev": true, + "license": "MIT", "dependencies": { - "@rushstack/terminal": "0.14.4", "@types/argparse": "1.0.38", "argparse": "~1.0.9", + "colors": "~1.2.1", "string-argv": "~0.3.1" } }, - "node_modules/@microsoft/api-extractor/node_modules/ajv": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.13.0.tgz", - "integrity": "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==", + "packages/blockly/node_modules/@microsoft/api-documenter/node_modules/ajv": { + "version": "6.12.6", "dev": true, + "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.3", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.4.1" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" }, "funding": { "type": "github", "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@microsoft/api-extractor/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "packages/blockly/node_modules/@microsoft/api-documenter/node_modules/argparse": { + "version": "1.0.10", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "packages/blockly/node_modules/@microsoft/api-documenter/node_modules/js-yaml": { + "version": "3.13.1", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "packages/blockly/node_modules/@microsoft/api-documenter/node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "packages/blockly/node_modules/@microsoft/api-documenter/node_modules/semver": { + "version": "7.3.8", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "packages/blockly/node_modules/@microsoft/api-extractor": { + "version": "7.52.13", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/api-extractor-model": "7.30.7", + "@microsoft/tsdoc": "~0.15.1", + "@microsoft/tsdoc-config": "~0.17.1", + "@rushstack/node-core-library": "5.14.0", + "@rushstack/rig-package": "0.5.3", + "@rushstack/terminal": "0.16.0", + "@rushstack/ts-command-line": "5.0.3", + "lodash": "~4.17.15", + "minimatch": "10.0.3", + "resolve": "~1.22.1", + "semver": "~7.5.4", + "source-map": "~0.6.1", + "typescript": "5.8.2" + }, + "bin": { + "api-extractor": "bin/api-extractor" + } + }, + "packages/blockly/node_modules/@microsoft/api-extractor-model": { + "version": "7.30.7", "dev": true, + "license": "MIT", "dependencies": { - "sprintf-js": "~1.0.2" + "@microsoft/tsdoc": "~0.15.1", + "@microsoft/tsdoc-config": "~0.17.1", + "@rushstack/node-core-library": "5.14.0" } }, - "node_modules/@microsoft/api-extractor/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "node_modules/@microsoft/api-extractor/node_modules/minimatch": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", - "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", + "packages/blockly/node_modules/@microsoft/api-extractor/node_modules/minimatch": { + "version": "10.0.3", "dev": true, + "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { - "node": "*" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@microsoft/api-extractor/node_modules/source-map": { + "packages/blockly/node_modules/@microsoft/api-extractor/node_modules/source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, - "node_modules/@microsoft/api-extractor/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/@microsoft/api-extractor/node_modules/typescript": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", - "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", + "packages/blockly/node_modules/@microsoft/api-extractor/node_modules/typescript": { + "version": "5.8.2", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -927,17 +2508,15 @@ "node": ">=14.17" } }, - "node_modules/@microsoft/tsdoc": { + "packages/blockly/node_modules/@microsoft/tsdoc": { "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz", - "integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/@microsoft/tsdoc-config": { + "packages/blockly/node_modules/@microsoft/tsdoc-config": { "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.17.1.tgz", - "integrity": "sha512-UtjIFe0C6oYgTnad4q1QP4qXwLhe6tIpNTRStJ2RZEPIkqQPREAwE5spzVxsdn9UaEMUqhh0AqSx3X4nWAKXWw==", "dev": true, + "license": "MIT", "dependencies": { "@microsoft/tsdoc": "0.15.1", "ajv": "~8.12.0", @@ -945,11 +2524,10 @@ "resolve": "~1.22.2" } }, - "node_modules/@microsoft/tsdoc-config/node_modules/ajv": { + "packages/blockly/node_modules/@microsoft/tsdoc-config/node_modules/ajv": { "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -961,16 +2539,8 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@microsoft/tsdoc-config/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "node_modules/@nodelib/fs.scandir": { + "packages/blockly/node_modules/@nodelib/fs.scandir": { "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, "license": "MIT", "dependencies": { @@ -981,20 +2551,16 @@ "node": ">= 8" } }, - "node_modules/@nodelib/fs.stat": { + "packages/blockly/node_modules/@nodelib/fs.stat": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, "license": "MIT", "engines": { "node": ">= 8" } }, - "node_modules/@nodelib/fs.walk": { + "packages/blockly/node_modules/@nodelib/fs.walk": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "license": "MIT", "dependencies": { @@ -1005,10 +2571,8 @@ "node": ">= 8" } }, - "node_modules/@pkgjs/parseargs": { + "packages/blockly/node_modules/@pkgjs/parseargs": { "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "dev": true, "license": "MIT", "optional": true, @@ -1016,22 +2580,19 @@ "node": ">=14" } }, - "node_modules/@pkgr/core": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", - "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "packages/blockly/node_modules/@pkgr/core": { + "version": "0.2.7", "dev": true, + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/unts" + "url": "https://opencollective.com/pkgr" } }, - "node_modules/@promptbook/utils": { + "packages/blockly/node_modules/@promptbook/utils": { "version": "0.69.5", - "resolved": "https://registry.npmjs.org/@promptbook/utils/-/utils-0.69.5.tgz", - "integrity": "sha512-xm5Ti/Hp3o4xHrsK9Yy3MS6KbDxYbq485hDsFvxqaNA7equHLPdo8H8faTitTeb14QCDfLW4iwCxdVYu5sn6YQ==", "dev": true, "funding": [ { @@ -1043,23 +2604,22 @@ "url": "https://github.com/webgptorg/promptbook/blob/main/README.md#%EF%B8%8F-contributing" } ], + "license": "CC-BY-4.0", "dependencies": { "spacetrim": "0.11.59" } }, - "node_modules/@puppeteer/browsers": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.6.1.tgz", - "integrity": "sha512-aBSREisdsGH890S2rQqK82qmQYU3uFpSH8wcZWHgHzl3LfzsxAKbLNiAG9mO8v1Y0UICBeClICxPJvyr0rcuxg==", + "packages/blockly/node_modules/@puppeteer/browsers": { + "version": "2.10.9", "dev": true, + "license": "Apache-2.0", "dependencies": { - "debug": "^4.4.0", + "debug": "^4.4.1", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", - "semver": "^7.6.3", - "tar-fs": "^3.0.6", - "unbzip2-stream": "^1.4.3", + "semver": "^7.7.2", + "tar-fs": "^3.1.0", "yargs": "^17.7.2" }, "bin": { @@ -1069,11 +2629,10 @@ "node": ">=18" } }, - "node_modules/@puppeteer/browsers/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "packages/blockly/node_modules/@puppeteer/browsers/node_modules/semver": { + "version": "7.7.2", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -1081,10 +2640,8 @@ "node": ">=10" } }, - "node_modules/@rushstack/node-core-library": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.11.0.tgz", - "integrity": "sha512-I8+VzG9A0F3nH2rLpPd7hF8F7l5Xb7D+ldrWVZYegXM6CsKkvWc670RlgK3WX8/AseZfXA/vVrh0bpXe2Y2UDQ==", + "packages/blockly/node_modules/@rushstack/node-core-library": { + "version": "5.14.0", "dev": true, "license": "MIT", "dependencies": { @@ -1106,10 +2663,8 @@ } } }, - "node_modules/@rushstack/node-core-library/node_modules/ajv": { + "packages/blockly/node_modules/@rushstack/node-core-library/node_modules/ajv": { "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.13.0.tgz", - "integrity": "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==", "dev": true, "license": "MIT", "dependencies": { @@ -1123,10 +2678,8 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@rushstack/node-core-library/node_modules/fs-extra": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", - "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", + "packages/blockly/node_modules/@rushstack/node-core-library/node_modules/fs-extra": { + "version": "11.3.2", "dev": true, "license": "MIT", "dependencies": { @@ -1138,31 +2691,21 @@ "node": ">=14.14" } }, - "node_modules/@rushstack/node-core-library/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rushstack/rig-package": { + "packages/blockly/node_modules/@rushstack/rig-package": { "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.5.3.tgz", - "integrity": "sha512-olzSSjYrvCNxUFZowevC3uz8gvKr3WTpHQ7BkpjtRpA3wK+T0ybep/SRUMfr195gBzJm5gaXw0ZMgjIyHqJUow==", "dev": true, + "license": "MIT", "dependencies": { "resolve": "~1.22.1", "strip-json-comments": "~3.1.1" } }, - "node_modules/@rushstack/terminal": { - "version": "0.14.6", - "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.14.6.tgz", - "integrity": "sha512-4nMUy4h0u5PGXVG71kEA9uYI3l8GjVqewoHOFONiM6fuqS51ORdaJZ5ZXB2VZEGUyfg1TOTSy88MF2cdAy+lqA==", + "packages/blockly/node_modules/@rushstack/terminal": { + "version": "0.16.0", "dev": true, "license": "MIT", "dependencies": { - "@rushstack/node-core-library": "5.11.0", + "@rushstack/node-core-library": "5.14.0", "supports-color": "~8.1.1" }, "peerDependencies": { @@ -1174,10 +2717,8 @@ } } }, - "node_modules/@rushstack/terminal/node_modules/supports-color": { + "packages/blockly/node_modules/@rushstack/terminal/node_modules/supports-color": { "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1190,175 +2731,152 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/@rushstack/ts-command-line": { - "version": "4.23.4", - "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.23.4.tgz", - "integrity": "sha512-pqmzDJCm0TS8VyeqnzcJ7ncwXgiLDQ6LVmXXfqv2nPL6VIz+UpyTpNVfZRJpyyJ+UDxqob1vIj2liaUfBjv8/A==", + "packages/blockly/node_modules/@rushstack/ts-command-line": { + "version": "5.0.3", "dev": true, "license": "MIT", "dependencies": { - "@rushstack/terminal": "0.14.6", + "@rushstack/terminal": "0.16.0", "@types/argparse": "1.0.38", "argparse": "~1.0.9", "string-argv": "~0.3.1" } }, - "node_modules/@rushstack/ts-command-line/node_modules/argparse": { + "packages/blockly/node_modules/@rushstack/ts-command-line/node_modules/argparse": { "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, "license": "MIT", "dependencies": { "sprintf-js": "~1.0.2" } }, - "node_modules/@sinonjs/commons": { + "packages/blockly/node_modules/@sinonjs/commons": { "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", - "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "type-detect": "4.0.8" } }, - "node_modules/@sinonjs/fake-timers": { + "packages/blockly/node_modules/@sinonjs/fake-timers": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", - "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^1.7.0" } }, - "node_modules/@sinonjs/samsam": { + "packages/blockly/node_modules/@sinonjs/samsam": { "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.3.1.tgz", - "integrity": "sha512-1Hc0b1TtyfBu8ixF/tpfSHTVWKwCBLY4QJbkgnE7HcwyvT2xArDxb4K7dMgqRm3szI+LJbzmW/s4xxEhv6hwDg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^1.6.0", "lodash.get": "^4.4.2", "type-detect": "^4.0.8" } }, - "node_modules/@sinonjs/text-encoding": { + "packages/blockly/node_modules/@sinonjs/text-encoding": { "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", - "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", - "dev": true + "dev": true, + "license": "(Unlicense OR Apache-2.0)" }, - "node_modules/@tootallnate/quickjs-emscripten": { + "packages/blockly/node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/@ts-stack/markdown": { + "packages/blockly/node_modules/@ts-stack/markdown": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@ts-stack/markdown/-/markdown-1.4.0.tgz", - "integrity": "sha512-z3fkD8wGSyqTCp+axZVlr9hFKyM18XKPHEyC8vmohyTcqf5sRRy9Sd0omYBJ85IDW57DLEcfvVatXfUt1unEew==", "dev": true, + "license": "MIT", "dependencies": { "tslib": "^2.0.0" } }, - "node_modules/@ts-stack/markdown/node_modules/tslib": { + "packages/blockly/node_modules/@ts-stack/markdown/node_modules/tslib": { "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", - "dev": true + "dev": true, + "license": "0BSD" }, - "node_modules/@types/argparse": { + "packages/blockly/node_modules/@types/argparse": { "version": "1.0.38", - "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", - "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true + "packages/blockly/node_modules/@types/estree": { + "version": "1.0.8", + "dev": true, + "license": "MIT" }, - "node_modules/@types/expect": { + "packages/blockly/node_modules/@types/expect": { "version": "1.20.4", - "resolved": "https://registry.npmjs.org/@types/expect/-/expect-1.20.4.tgz", - "integrity": "sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/@types/json-schema": { + "packages/blockly/node_modules/@types/json-schema": { "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/@types/node": { - "version": "20.16.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.3.tgz", - "integrity": "sha512-/wdGiWRkMOm53gAsSyFMXFZHbVg7C6CbkrzHNpaHoYfsUWPg7m6ZRKtvQjgvQ9i8WT540a3ydRlRQbxjY30XxQ==", + "packages/blockly/node_modules/@types/node": { + "version": "20.19.21", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.21.0" } }, - "node_modules/@types/sinonjs__fake-timers": { + "packages/blockly/node_modules/@types/sinonjs__fake-timers": { "version": "8.1.5", - "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", - "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/@types/vinyl": { + "packages/blockly/node_modules/@types/vinyl": { "version": "2.0.12", - "resolved": "https://registry.npmjs.org/@types/vinyl/-/vinyl-2.0.12.tgz", - "integrity": "sha512-Sr2fYMBUVGYq8kj3UthXFAu5UN6ZW+rYr4NACjZQJvHvj+c8lYv0CahmZ2P/r7iUkN44gGUBwqxZkrKXYPb7cw==", "dev": true, + "license": "MIT", "dependencies": { "@types/expect": "^1.20.4", "@types/node": "*" } }, - "node_modules/@types/which": { + "packages/blockly/node_modules/@types/which": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@types/which/-/which-2.0.2.tgz", - "integrity": "sha512-113D3mDkZDjo+EeUEHCFy0qniNc1ZpecGiAU7WSo7YDoSzolZIQKpYFHrPpjkB2nuyahcKfrmLXeQlh7gqJYdw==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/@types/ws": { - "version": "8.5.13", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", - "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", + "packages/blockly/node_modules/@types/ws": { + "version": "8.18.1", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } }, - "node_modules/@types/yauzl": { + "packages/blockly/node_modules/@types/yauzl": { "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "@types/node": "*" } }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.23.0.tgz", - "integrity": "sha512-vBz65tJgRrA1Q5gWlRfvoH+w943dq9K1p1yDBY2pc+a1nbBLZp7fB9+Hk8DaALUbzjqlMfgaqlVPT1REJdkt/w==", + "packages/blockly/node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.46.2", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.23.0", - "@typescript-eslint/type-utils": "8.23.0", - "@typescript-eslint/utils": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0", + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/type-utils": "8.46.2", + "@typescript-eslint/utils": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", "graphemer": "^1.4.0", - "ignore": "^5.3.1", + "ignore": "^7.0.0", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1368,22 +2886,28 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "@typescript-eslint/parser": "^8.46.2", "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <6.0.0" + } + }, + "packages/blockly/node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" } }, - "node_modules/@typescript-eslint/parser": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.23.0.tgz", - "integrity": "sha512-h2lUByouOXFAlMec2mILeELUbME5SZRN/7R9Cw2RD2lRQQY08MWMM+PmVVKKJNK1aIwqTo9t/0CvOxwPbRIE2Q==", + "packages/blockly/node_modules/@typescript-eslint/parser": { + "version": "8.46.2", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.23.0", - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/typescript-estree": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0", + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", "debug": "^4.3.4" }, "engines": { @@ -1395,18 +2919,36 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <6.0.0" + } + }, + "packages/blockly/node_modules/@typescript-eslint/project-service": { + "version": "8.46.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.46.2", + "@typescript-eslint/types": "^8.46.2", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.23.0.tgz", - "integrity": "sha512-OGqo7+dXHqI7Hfm+WqkZjKjsiRtFUQHPdGMXzk5mYXhJUedO7e/Y7i8AK3MyLMgZR93TX4bIzYrfyVjLC+0VSw==", + "packages/blockly/node_modules/@typescript-eslint/scope-manager": { + "version": "8.46.2", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0" + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1416,17 +2958,31 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.23.0.tgz", - "integrity": "sha512-iIuLdYpQWZKbiH+RkCGc6iu+VwscP5rCtQ1lyQ7TYuKLrcZoeJVpcLiG8DliXVkUxirW/PWlmS+d6yD51L9jvA==", + "packages/blockly/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.46.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "packages/blockly/node_modules/@typescript-eslint/type-utils": { + "version": "8.46.2", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.23.0", - "@typescript-eslint/utils": "8.23.0", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/utils": "8.46.2", "debug": "^4.3.4", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1437,13 +2993,11 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/types": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.23.0.tgz", - "integrity": "sha512-1sK4ILJbCmZOTt9k4vkoulT6/y5CHJ1qUYxqpF1K/DBAd8+ZUL4LlSCxOssuH5m4rUaaN0uS0HlVPvd45zjduQ==", + "packages/blockly/node_modules/@typescript-eslint/types": { + "version": "8.46.2", "dev": true, "license": "MIT", "engines": { @@ -1454,21 +3008,21 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.23.0.tgz", - "integrity": "sha512-LcqzfipsB8RTvH8FX24W4UUFk1bl+0yTOf9ZA08XngFwMg4Kj8A+9hwz8Cr/ZS4KwHrmo9PJiLZkOt49vPnuvQ==", + "packages/blockly/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.46.2", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0", + "@typescript-eslint/project-service": "8.46.2", + "@typescript-eslint/tsconfig-utils": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1478,23 +3032,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "packages/blockly/node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "packages/blockly/node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "license": "ISC", "dependencies": { @@ -1507,10 +3057,8 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "packages/blockly/node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", "dev": true, "license": "ISC", "bin": { @@ -1520,17 +3068,15 @@ "node": ">=10" } }, - "node_modules/@typescript-eslint/utils": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.23.0.tgz", - "integrity": "sha512-uB/+PSo6Exu02b5ZEiVtmY6RVYO7YU5xqgzTIVZwTHvvK3HsL8tZZHFaTLFtRG3CsV4A5mhOv+NZx5BlhXPyIA==", + "packages/blockly/node_modules/@typescript-eslint/utils": { + "version": "8.46.2", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.23.0", - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/typescript-estree": "8.23.0" + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1541,18 +3087,16 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.23.0.tgz", - "integrity": "sha512-oWWhcWDLwDfu++BGTZcmXWqpwtkwb5o7fxUIGksMQQDSdPW9prsSnfIOZMlsj4vBOSrcnjIUZMiIjODgGosFhQ==", + "packages/blockly/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.46.2", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.23.0", - "eslint-visitor-keys": "^4.2.0" + "@typescript-eslint/types": "8.46.2", + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1562,10 +3106,8 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "packages/blockly/node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.1", "dev": true, "license": "Apache-2.0", "engines": { @@ -1575,16 +3117,14 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@wdio/config": { - "version": "9.4.2", - "resolved": "https://registry.npmjs.org/@wdio/config/-/config-9.4.2.tgz", - "integrity": "sha512-pTwxN0EmkHzYKTfYnk/d+qIBRJFrNxNa58PDPbAc0On4dq7qqueDm/HyyFnTXc6t0vrqCNlR55DmKwqt3UqsKQ==", + "packages/blockly/node_modules/@wdio/config": { + "version": "9.14.0", "dev": true, + "license": "MIT", "dependencies": { - "@wdio/logger": "9.1.3", - "@wdio/types": "9.4.2", - "@wdio/utils": "9.4.2", - "decamelize": "^6.0.0", + "@wdio/logger": "9.4.4", + "@wdio/types": "9.14.0", + "@wdio/utils": "9.14.0", "deepmerge-ts": "^7.0.3", "glob": "^10.2.2", "import-meta-resolve": "^4.0.0" @@ -1593,20 +3133,16 @@ "node": ">=18.20.0" } }, - "node_modules/@wdio/config/node_modules/brace-expansion": { + "packages/blockly/node_modules/@wdio/config/node_modules/brace-expansion": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, - "node_modules/@wdio/config/node_modules/glob": { + "packages/blockly/node_modules/@wdio/config/node_modules/glob": { "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, "license": "ISC", "dependencies": { @@ -1624,10 +3160,8 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@wdio/config/node_modules/jackspeak": { + "packages/blockly/node_modules/@wdio/config/node_modules/jackspeak": { "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -1640,17 +3174,13 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/@wdio/config/node_modules/lru-cache": { + "packages/blockly/node_modules/@wdio/config/node_modules/lru-cache": { "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, "license": "ISC" }, - "node_modules/@wdio/config/node_modules/minimatch": { + "packages/blockly/node_modules/@wdio/config/node_modules/minimatch": { "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "license": "ISC", "dependencies": { @@ -1663,10 +3193,8 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@wdio/config/node_modules/path-scurry": { + "packages/blockly/node_modules/@wdio/config/node_modules/path-scurry": { "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -1680,11 +3208,10 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@wdio/logger": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.1.3.tgz", - "integrity": "sha512-cumRMK/gE1uedBUw3WmWXOQ7HtB6DR8EyKQioUz2P0IJtRRpglMBdZV7Svr3b++WWawOuzZHMfbTkJQmaVt8Gw==", + "packages/blockly/node_modules/@wdio/logger": { + "version": "9.4.4", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^5.1.2", "loglevel": "^1.6.0", @@ -1695,11 +3222,10 @@ "node": ">=18.20.0" } }, - "node_modules/@wdio/logger/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "packages/blockly/node_modules/@wdio/logger/node_modules/chalk": { + "version": "5.4.1", "dev": true, + "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" }, @@ -1707,11 +3233,10 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@wdio/logger/node_modules/strip-ansi": { + "packages/blockly/node_modules/@wdio/logger/node_modules/strip-ansi": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -1722,17 +3247,15 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/@wdio/protocols": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-9.2.2.tgz", - "integrity": "sha512-0GMUSHCbYm+J+rnRU6XPtaUgVCRICsiH6W5zCXpePm3wLlbmg/mvZ+4OnNErssbpIOulZuAmC2jNmut2AEfWSw==", - "dev": true + "packages/blockly/node_modules/@wdio/protocols": { + "version": "9.14.0", + "dev": true, + "license": "MIT" }, - "node_modules/@wdio/repl": { - "version": "9.0.8", - "resolved": "https://registry.npmjs.org/@wdio/repl/-/repl-9.0.8.tgz", - "integrity": "sha512-3iubjl4JX5zD21aFxZwQghqC3lgu+mSs8c3NaiYYNCC+IT5cI/8QuKlgh9s59bu+N3gG988jqMJeCYlKuUv/iw==", + "packages/blockly/node_modules/@wdio/repl": { + "version": "9.4.4", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "^20.1.0" }, @@ -1740,11 +3263,10 @@ "node": ">=18.20.0" } }, - "node_modules/@wdio/types": { - "version": "9.4.2", - "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.4.2.tgz", - "integrity": "sha512-eaEqngtnYQ2D6YLbfM2HGJVbJBjO1fAotPdu1G1cr28RAjcsANpl5IbDHZMlsKmiU9JtJx5CadhxCnrCLZVVDw==", + "packages/blockly/node_modules/@wdio/types": { + "version": "9.14.0", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "^20.1.0" }, @@ -1752,15 +3274,14 @@ "node": ">=18.20.0" } }, - "node_modules/@wdio/utils": { - "version": "9.4.2", - "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.4.2.tgz", - "integrity": "sha512-ryjPjmyR0s/9qgBBLKvh1cP/OndJ2BkY2LZhc4Nx6z9IxKmKDq79ajKNmsWR55mTMcembhcb8ikCACW8tEGEdA==", + "packages/blockly/node_modules/@wdio/utils": { + "version": "9.14.0", "dev": true, + "license": "MIT", "dependencies": { "@puppeteer/browsers": "^2.2.0", - "@wdio/logger": "9.1.3", - "@wdio/types": "9.4.2", + "@wdio/logger": "9.4.4", + "@wdio/types": "9.14.0", "decamelize": "^6.0.0", "deepmerge-ts": "^7.0.3", "edgedriver": "^6.1.1", @@ -1776,28 +3297,25 @@ "node": ">=18.20.0" } }, - "node_modules/@yarnpkg/lockfile": { + "packages/blockly/node_modules/@yarnpkg/lockfile": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", - "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", - "dev": true + "dev": true, + "license": "BSD-2-Clause" }, - "node_modules/@zip.js/zip.js": { - "version": "2.7.54", - "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.54.tgz", - "integrity": "sha512-qMrJVg2hoEsZJjMJez9yI2+nZlBUxgYzGV3mqcb2B/6T1ihXp0fWBDYlVHlHquuorgNUQP5a8qSmX6HF5rFJNg==", + "packages/blockly/node_modules/@zip.js/zip.js": { + "version": "2.7.61", "dev": true, + "license": "BSD-3-Clause", "engines": { "bun": ">=0.7.0", "deno": ">=1.0.0", "node": ">=16.5.0" } }, - "node_modules/abort-controller": { + "packages/blockly/node_modules/abort-controller": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", "dev": true, + "license": "MIT", "dependencies": { "event-target-shim": "^5.0.0" }, @@ -1805,11 +3323,10 @@ "node": ">=6.5" } }, - "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "packages/blockly/node_modules/acorn": { + "version": "8.15.0", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -1817,44 +3334,25 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-jsx": { + "packages/blockly/node_modules/acorn-jsx": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/agent-base": { + "packages/blockly/node_modules/agent-base": { "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", "engines": { "node": ">= 14" } }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-draft-04": { + "packages/blockly/node_modules/ajv-draft-04": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", - "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", "dev": true, + "license": "MIT", "peerDependencies": { "ajv": "^8.5.0" }, @@ -1864,11 +3362,10 @@ } } }, - "node_modules/ajv-formats": { + "packages/blockly/node_modules/ajv-formats": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "^8.0.0" }, @@ -1881,42 +3378,10 @@ } } }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.14.0.tgz", - "integrity": "sha512-oYs1UUtO97ZO2lJ4bwnWeQW8/zvOIQLGKcvPTsWmvc2SYgBb+upuNS5NxoLaMU4h8Ju3Nbj6Cq8mD2LQoqVKFA==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.4.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-gray": { + "packages/blockly/node_modules/ansi-gray": { "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", - "integrity": "sha1-KWLPVOyXksSFEKPetSRDaGHvclE=", "dev": true, + "license": "MIT", "dependencies": { "ansi-wrap": "0.1.0" }, @@ -1924,11 +3389,10 @@ "node": ">=0.10.0" } }, - "node_modules/ansi-regex": { + "packages/blockly/node_modules/ansi-regex": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -1936,41 +3400,23 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/ansi-wrap": { + "packages/blockly/node_modules/ansi-wrap": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", - "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/any-promise": { + "packages/blockly/node_modules/any-promise": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/anymatch": { + "packages/blockly/node_modules/anymatch": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, + "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -1979,11 +3425,10 @@ "node": ">= 8" } }, - "node_modules/archiver": { + "packages/blockly/node_modules/archiver": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", - "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", "dev": true, + "license": "MIT", "dependencies": { "archiver-utils": "^5.0.2", "async": "^3.2.4", @@ -1997,11 +3442,10 @@ "node": ">= 14" } }, - "node_modules/archiver-utils": { + "packages/blockly/node_modules/archiver-utils": { "version": "5.0.2", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", - "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", "dev": true, + "license": "MIT", "dependencies": { "glob": "^10.0.0", "graceful-fs": "^4.2.0", @@ -2015,44 +3459,16 @@ "node": ">= 14" } }, - "node_modules/archiver-utils/node_modules/brace-expansion": { + "packages/blockly/node_modules/archiver-utils/node_modules/brace-expansion": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, - "node_modules/archiver-utils/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/archiver-utils/node_modules/glob": { + "packages/blockly/node_modules/archiver-utils/node_modules/glob": { "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, "license": "ISC", "dependencies": { @@ -2070,10 +3486,8 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/archiver-utils/node_modules/jackspeak": { + "packages/blockly/node_modules/archiver-utils/node_modules/jackspeak": { "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -2086,17 +3500,13 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/archiver-utils/node_modules/lru-cache": { + "packages/blockly/node_modules/archiver-utils/node_modules/lru-cache": { "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, "license": "ISC" }, - "node_modules/archiver-utils/node_modules/minimatch": { + "packages/blockly/node_modules/archiver-utils/node_modules/minimatch": { "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "license": "ISC", "dependencies": { @@ -2109,10 +3519,8 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/archiver-utils/node_modules/path-scurry": { + "packages/blockly/node_modules/archiver-utils/node_modules/path-scurry": { "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -2126,11 +3534,10 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/archiver-utils/node_modules/readable-stream": { + "packages/blockly/node_modules/archiver-utils/node_modules/readable-stream": { "version": "4.5.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", - "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", "dev": true, + "license": "MIT", "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", @@ -2142,39 +3549,8 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/archiver-utils/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/archiver-utils/node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/archiver/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "packages/blockly/node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.2.1", "dev": true, "funding": [ { @@ -2190,25 +3566,28 @@ "url": "https://feross.org/support" } ], + "license": "MIT" + }, + "packages/blockly/node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.3.0", + "dev": true, + "license": "MIT", "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" + "safe-buffer": "~5.2.0" } }, - "node_modules/archiver/node_modules/buffer-crc32": { + "packages/blockly/node_modules/archiver/node_modules/buffer-crc32": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", - "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.0.0" } }, - "node_modules/archiver/node_modules/readable-stream": { + "packages/blockly/node_modules/archiver/node_modules/readable-stream": { "version": "4.5.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", - "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", "dev": true, + "license": "MIT", "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", @@ -2220,10 +3599,8 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/archiver/node_modules/safe-buffer": { + "packages/blockly/node_modules/archiver/node_modules/safe-buffer": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true, "funding": [ { @@ -2238,100 +3615,77 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, - "node_modules/archiver/node_modules/string_decoder": { + "packages/blockly/node_modules/archiver/node_modules/string_decoder": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" } }, - "node_modules/are-docs-informative": { + "packages/blockly/node_modules/are-docs-informative": { "version": "0.0.2", - "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", - "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", "dev": true, + "license": "MIT", "engines": { "node": ">=14" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/aria-query": { + "packages/blockly/node_modules/aria-query": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, + "license": "Apache-2.0", "dependencies": { "dequal": "^2.0.3" } }, - "node_modules/arr-diff": { + "packages/blockly/node_modules/arr-diff": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/arr-union": { + "packages/blockly/node_modules/arr-union": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/array-each": { + "packages/blockly/node_modules/array-each": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", - "integrity": "sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/array-slice": { + "packages/blockly/node_modules/array-slice": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", - "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/assign-symbols": { + "packages/blockly/node_modules/assign-symbols": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/ast-types": { + "packages/blockly/node_modules/ast-types": { "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", "dev": true, + "license": "MIT", "dependencies": { "tslib": "^2.0.1" }, @@ -2339,23 +3693,20 @@ "node": ">=4" } }, - "node_modules/ast-types/node_modules/tslib": { + "packages/blockly/node_modules/ast-types/node_modules/tslib": { "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true + "dev": true, + "license": "0BSD" }, - "node_modules/async": { + "packages/blockly/node_modules/async": { "version": "3.2.5", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/async-done": { + "packages/blockly/node_modules/async-done": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/async-done/-/async-done-2.0.0.tgz", - "integrity": "sha512-j0s3bzYq9yKIVLKGE/tWlCpa3PfFLcrDZLTSVdnnCTGagXuXBJO4SsY9Xdk/fQBirCkH4evW5xOeJXqlAQFdsw==", "dev": true, + "license": "MIT", "dependencies": { "end-of-stream": "^1.4.4", "once": "^1.4.0", @@ -2365,11 +3716,10 @@ "node": ">= 10.13.0" } }, - "node_modules/async-settle": { + "packages/blockly/node_modules/async-settle": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-2.0.0.tgz", - "integrity": "sha512-Obu/KE8FurfQRN6ODdHN9LuXqwC+JFIM9NRyZqJJ4ZfLJmIYN9Rg0/kb+wF70VV5+fJusTMQlJ1t5rF7J/ETdg==", "dev": true, + "license": "MIT", "dependencies": { "async-done": "^2.0.0" }, @@ -2377,25 +3727,10 @@ "node": ">= 10.13.0" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "dev": true, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/atob": { + "packages/blockly/node_modules/atob": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", "dev": true, + "license": "(MIT OR Apache-2.0)", "bin": { "atob": "bin/atob.js" }, @@ -2403,17 +3738,10 @@ "node": ">= 4.5.0" } }, - "node_modules/b4a": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", - "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", - "dev": true - }, - "node_modules/bach": { + "packages/blockly/node_modules/bach": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/bach/-/bach-2.0.1.tgz", - "integrity": "sha512-A7bvGMGiTOxGMpNupYl9HQTf0FFDNF4VCmks4PJpFyN1AX2pdKuxuwdvUz2Hu388wcgp+OvGFNsumBfFNkR7eg==", "dev": true, + "license": "MIT", "dependencies": { "async-done": "^2.0.0", "async-settle": "^2.0.0", @@ -2423,62 +3751,74 @@ "node": ">=10.13.0" } }, - "node_modules/balanced-match": { + "packages/blockly/node_modules/balanced-match": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/bare-events": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.3.1.tgz", - "integrity": "sha512-sJnSOTVESURZ61XgEleqmP255T6zTYwHPwE4r6SssIh0U9/uDvfpdoJYpVUerJJZH2fueO+CdT8ZT+OC/7aZDA==", "dev": true, - "optional": true + "license": "MIT" }, - "node_modules/bare-fs": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.5.tgz", - "integrity": "sha512-SlE9eTxifPDJrT6YgemQ1WGFleevzwY+XAP1Xqgl56HtcrisC2CHCZ2tq6dBpcH2TnNxwUEUGhweo+lrQtYuiw==", + "packages/blockly/node_modules/bare-fs": { + "version": "4.1.5", "dev": true, + "license": "Apache-2.0", "optional": true, "dependencies": { - "bare-events": "^2.0.0", - "bare-path": "^2.0.0", - "bare-stream": "^2.0.0" + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } } }, - "node_modules/bare-os": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.4.tgz", - "integrity": "sha512-z3UiI2yi1mK0sXeRdc4O1Kk8aOa/e+FNWZcTiPB/dfTWyLypuE99LibgRaQki914Jq//yAWylcAt+mknKdixRQ==", + "packages/blockly/node_modules/bare-os": { + "version": "3.6.1", "dev": true, - "optional": true + "license": "Apache-2.0", + "optional": true, + "engines": { + "bare": ">=1.14.0" + } }, - "node_modules/bare-path": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz", - "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==", + "packages/blockly/node_modules/bare-path": { + "version": "3.0.0", "dev": true, + "license": "Apache-2.0", "optional": true, "dependencies": { - "bare-os": "^2.1.0" + "bare-os": "^3.0.1" } }, - "node_modules/bare-stream": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.1.tgz", - "integrity": "sha512-eVZbtKM+4uehzrsj49KtCy3Pbg7kO1pJ3SKZ1SFrIH/0pnj9scuGGgUlNDf/7qS8WKtGdiJY5Kyhs/ivYPTB/g==", + "packages/blockly/node_modules/bare-stream": { + "version": "2.6.5", "dev": true, + "license": "Apache-2.0", "optional": true, "dependencies": { "streamx": "^2.21.0" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } } }, - "node_modules/base64-js": { + "packages/blockly/node_modules/base64-js": { "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true, "funding": [ { @@ -2493,13 +3833,13 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, - "node_modules/basic-auth": { + "packages/blockly/node_modules/basic-auth": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", - "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "5.1.2" }, @@ -2507,29 +3847,26 @@ "node": ">= 0.8" } }, - "node_modules/basic-ftp": { + "packages/blockly/node_modules/basic-ftp": { "version": "5.0.5", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", - "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" } }, - "node_modules/binary-extensions": { + "packages/blockly/node_modules/binary-extensions": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/binaryextensions": { + "packages/blockly/node_modules/binaryextensions": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-2.3.0.tgz", - "integrity": "sha512-nAihlQsYGyc5Bwq6+EsubvANYGExeJKHDO3RjnvwU042fawQTQfM3Kxn7IHUXQOz4bzfwsGYYHGSvXyW4zOGLg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8" }, @@ -2537,62 +3874,34 @@ "url": "https://bevry.me/fund" } }, - "node_modules/bl": { + "packages/blockly/node_modules/bl": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", - "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", "dev": true, + "license": "MIT", "dependencies": { "buffer": "^6.0.3", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, - "node_modules/bl/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/boolbase": { + "packages/blockly/node_modules/boolbase": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true + "dev": true, + "license": "ISC" }, - "node_modules/brace-expansion": { + "packages/blockly/node_modules/brace-expansion": { "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, - "node_modules/braces": { + "packages/blockly/node_modules/braces": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { "fill-range": "^7.1.1" }, @@ -2600,16 +3909,13 @@ "node": ">=8" } }, - "node_modules/browser-stdout": { + "packages/blockly/node_modules/browser-stdout": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true + "dev": true, + "license": "ISC" }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "packages/blockly/node_modules/buffer": { + "version": "6.0.3", "dev": true, "funding": [ { @@ -2625,65 +3931,72 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "base64-js": "^1.3.1", - "ieee754": "^1.1.13" + "ieee754": "^1.2.1" } }, - "node_modules/buffer-crc32": { + "packages/blockly/node_modules/buffer-crc32": { "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", "dev": true, + "license": "MIT", "engines": { "node": "*" } }, - "node_modules/buffer-from": { + "packages/blockly/node_modules/buffer-from": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/bytes": { + "packages/blockly/node_modules/bytes": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "packages/blockly/node_modules/call-bind-apply-helpers": { + "version": "1.0.2", "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, "engines": { - "node": ">=6" + "node": ">= 0.4" } }, - "node_modules/chai": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz", - "integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==", + "packages/blockly/node_modules/call-bound": { + "version": "1.0.4", "dev": true, + "license": "MIT", "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { - "node": ">=12" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/chalk": { + "packages/blockly/node_modules/chai": { + "version": "6.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "packages/blockly/node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -2695,20 +4008,10 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/check-error": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", - "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", - "dev": true, - "engines": { - "node": ">= 16" - } - }, - "node_modules/cheerio": { + "packages/blockly/node_modules/cheerio": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", - "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", "dev": true, + "license": "MIT", "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", @@ -2729,11 +4032,10 @@ "url": "https://github.com/cheeriojs/cheerio?sponsor=1" } }, - "node_modules/cheerio-select": { + "packages/blockly/node_modules/cheerio-select": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", - "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", @@ -2746,10 +4048,8 @@ "url": "https://github.com/sponsors/fb55" } }, - "node_modules/chokidar": { + "packages/blockly/node_modules/chokidar": { "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", "dev": true, "funding": [ { @@ -2757,6 +4057,7 @@ "url": "https://paulmillr.com/funding/" } ], + "license": "MIT", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -2773,10 +4074,20 @@ "fsevents": "~2.3.2" } }, - "node_modules/ci-info": { + "packages/blockly/node_modules/chromium-bidi": { + "version": "8.0.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "packages/blockly/node_modules/ci-info": { "version": "3.8.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", - "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", "dev": true, "funding": [ { @@ -2784,61 +4095,48 @@ "url": "https://github.com/sponsors/sibiraj-s" } ], + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/cliui": { + "packages/blockly/node_modules/cliui": { "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, - "node_modules/clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", - "dev": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/clone-buffer": { + "packages/blockly/node_modules/clone-buffer": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", - "integrity": "sha1-4+JbIHrE5wGvch4staFnksrD3Fg=", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.10" } }, - "node_modules/clone-stats": { + "packages/blockly/node_modules/clone-stats": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", - "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/cloneable-readable": { + "packages/blockly/node_modules/cloneable-readable": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.3.tgz", - "integrity": "sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ==", "dev": true, + "license": "MIT", "dependencies": { "inherits": "^2.0.1", "process-nextick-args": "^2.0.0", "readable-stream": "^2.3.5" } }, - "node_modules/cloneable-readable/node_modules/readable-stream": { + "packages/blockly/node_modules/cloneable-readable/node_modules/readable-stream": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, + "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -2849,67 +4147,42 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/color-support": { + "packages/blockly/node_modules/color-support": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", "dev": true, + "license": "ISC", "bin": { "color-support": "bin.js" } }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, + "packages/blockly/node_modules/colors": { + "version": "1.2.5", + "dev": true, + "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">=0.1.90" } }, - "node_modules/commander": { + "packages/blockly/node_modules/commander": { "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", "dev": true, + "license": "MIT", "engines": { "node": "^12.20.0 || >=14" } }, - "node_modules/comment-parser": { + "packages/blockly/node_modules/comment-parser": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", - "integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 12.0.0" } }, - "node_modules/compress-commons": { + "packages/blockly/node_modules/compress-commons": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", - "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", "dev": true, + "license": "MIT", "dependencies": { "crc-32": "^1.2.0", "crc32-stream": "^6.0.0", @@ -2921,35 +4194,10 @@ "node": ">= 14" } }, - "node_modules/compress-commons/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/compress-commons/node_modules/readable-stream": { + "packages/blockly/node_modules/compress-commons/node_modules/readable-stream": { "version": "4.5.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", - "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", "dev": true, + "license": "MIT", "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", @@ -2961,10 +4209,8 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/compress-commons/node_modules/safe-buffer": { + "packages/blockly/node_modules/compress-commons/node_modules/safe-buffer": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true, "funding": [ { @@ -2979,31 +4225,29 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, - "node_modules/compress-commons/node_modules/string_decoder": { + "packages/blockly/node_modules/compress-commons/node_modules/string_decoder": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" } }, - "node_modules/concat-map": { + "packages/blockly/node_modules/concat-map": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/concat-stream": { + "packages/blockly/node_modules/concat-stream": { "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", "dev": true, "engines": [ "node >= 0.8" ], + "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", @@ -3011,11 +4255,10 @@ "typedarray": "^0.0.6" } }, - "node_modules/concat-stream/node_modules/readable-stream": { + "packages/blockly/node_modules/concat-stream/node_modules/readable-stream": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, + "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -3026,37 +4269,33 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/concat-with-sourcemaps": { + "packages/blockly/node_modules/concat-with-sourcemaps": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz", - "integrity": "sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==", "dev": true, + "license": "ISC", "dependencies": { "source-map": "^0.6.1" } }, - "node_modules/concat-with-sourcemaps/node_modules/source-map": { + "packages/blockly/node_modules/concat-with-sourcemaps/node_modules/source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, - "node_modules/concurrently": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.2.tgz", - "integrity": "sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==", + "packages/blockly/node_modules/concurrently": { + "version": "9.2.1", "dev": true, + "license": "MIT", "dependencies": { - "chalk": "^4.1.2", - "lodash": "^4.17.21", - "rxjs": "^7.8.1", - "shell-quote": "^1.8.1", - "supports-color": "^8.1.1", - "tree-kill": "^1.2.2", - "yargs": "^17.7.2" + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", @@ -3069,11 +4308,10 @@ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, - "node_modules/concurrently/node_modules/supports-color": { + "packages/blockly/node_modules/concurrently/node_modules/supports-color": { "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -3084,29 +4322,26 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/content-type": { + "packages/blockly/node_modules/content-type": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } }, - "node_modules/convert-source-map": { + "packages/blockly/node_modules/convert-source-map": { "version": "1.8.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", - "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "~5.1.1" } }, - "node_modules/copy-props": { + "packages/blockly/node_modules/copy-props": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-4.0.0.tgz", - "integrity": "sha512-bVWtw1wQLzzKiYROtvNlbJgxgBYt2bMJpkCbKmXM3xyijvcjjWXEk5nyrrT3bgJ7ODb19ZohE2T0Y3FgNPyoTw==", "dev": true, + "license": "MIT", "dependencies": { "each-props": "^3.0.0", "is-plain-object": "^5.0.0" @@ -3115,26 +4350,23 @@ "node": ">= 10.13.0" } }, - "node_modules/core-util-is": { + "packages/blockly/node_modules/core-util-is": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/corser": { + "packages/blockly/node_modules/corser": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", - "integrity": "sha1-jtolLsqrWEDc2XXOuQ2TcMgZ/4c=", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4.0" } }, - "node_modules/crc-32": { + "packages/blockly/node_modules/crc-32": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", - "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", "dev": true, + "license": "Apache-2.0", "bin": { "crc32": "bin/crc32.njs" }, @@ -3142,11 +4374,10 @@ "node": ">=0.8" } }, - "node_modules/crc32-stream": { + "packages/blockly/node_modules/crc32-stream": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", - "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", "dev": true, + "license": "MIT", "dependencies": { "crc-32": "^1.2.0", "readable-stream": "^4.0.0" @@ -3155,35 +4386,10 @@ "node": ">= 14" } }, - "node_modules/crc32-stream/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/crc32-stream/node_modules/readable-stream": { + "packages/blockly/node_modules/crc32-stream/node_modules/readable-stream": { "version": "4.5.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", - "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", "dev": true, + "license": "MIT", "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", @@ -3195,10 +4401,8 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/crc32-stream/node_modules/safe-buffer": { + "packages/blockly/node_modules/crc32-stream/node_modules/safe-buffer": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true, "funding": [ { @@ -3213,22 +4417,21 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, - "node_modules/crc32-stream/node_modules/string_decoder": { + "packages/blockly/node_modules/crc32-stream/node_modules/string_decoder": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" } }, - "node_modules/cross-spawn": { + "packages/blockly/node_modules/cross-spawn": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -3238,22 +4441,20 @@ "node": ">= 8" } }, - "node_modules/css": { + "packages/blockly/node_modules/css": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz", - "integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==", "dev": true, + "license": "MIT", "dependencies": { "inherits": "^2.0.4", "source-map": "^0.6.1", "source-map-resolve": "^0.6.0" } }, - "node_modules/css-select": { + "packages/blockly/node_modules/css-select": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", - "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", @@ -3265,23 +4466,18 @@ "url": "https://github.com/sponsors/fb55" } }, - "node_modules/css-shorthand-properties": { + "packages/blockly/node_modules/css-shorthand-properties": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/css-shorthand-properties/-/css-shorthand-properties-1.1.1.tgz", - "integrity": "sha512-Md+Juc7M3uOdbAFwOYlTrccIZ7oCFuzrhKYQjdeUEW/sE1hv17Jp/Bws+ReOPpGVBTYCBoYo+G17V5Qo8QQ75A==", "dev": true }, - "node_modules/css-value": { + "packages/blockly/node_modules/css-value": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/css-value/-/css-value-0.0.1.tgz", - "integrity": "sha1-Xv1sLupeof1rasV+wEJ7GEUkJOo=", "dev": true }, - "node_modules/css-what": { + "packages/blockly/node_modules/css-what": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">= 6" }, @@ -3289,55 +4485,50 @@ "url": "https://github.com/sponsors/fb55" } }, - "node_modules/css/node_modules/source-map": { + "packages/blockly/node_modules/css/node_modules/source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, - "node_modules/cssstyle": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.1.0.tgz", - "integrity": "sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==", + "packages/blockly/node_modules/cssstyle": { + "version": "4.3.0", + "license": "MIT", "dependencies": { - "rrweb-cssom": "^0.7.1" + "@asamuzakjp/css-color": "^3.1.1", + "rrweb-cssom": "^0.8.0" }, "engines": { "node": ">=18" } }, - "node_modules/d": { + "packages/blockly/node_modules/d": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", - "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", "dev": true, + "license": "ISC", "dependencies": { "es5-ext": "^0.10.50", "type": "^1.0.1" } }, - "node_modules/dat.gui": { + "packages/blockly/node_modules/dat.gui": { "version": "0.7.7", - "resolved": "https://registry.npmjs.org/dat.gui/-/dat.gui-0.7.7.tgz", - "integrity": "sha512-sRl/28gF/XRC5ywC9I4zriATTsQcpSsRG7seXCPnTkK8/EQMIbCu5NPMpICLGxX9ZEUvcXR3ArLYCtgreFoMDw==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, - "node_modules/data-uri-to-buffer": { + "packages/blockly/node_modules/data-uri-to-buffer": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 12" } }, - "node_modules/data-urls": { + "packages/blockly/node_modules/data-urls": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", - "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "license": "MIT", "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0" @@ -3346,33 +4537,9 @@ "node": ">=18" } }, - "node_modules/data-urls/node_modules/tr46": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", - "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/data-urls/node_modules/whatwg-url": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", - "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", - "dependencies": { - "tr46": "^5.0.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "packages/blockly/node_modules/debug": { + "version": "4.4.1", + "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -3385,31 +4552,28 @@ } } }, - "node_modules/debug-fabulous": { + "packages/blockly/node_modules/debug-fabulous": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/debug-fabulous/-/debug-fabulous-1.1.0.tgz", - "integrity": "sha512-GZqvGIgKNlUnHUPQhepnUZFIMoi3dgZKQBzKDeL2g7oJF9SNAji/AAu36dusFUas0O+pae74lNeoIPHqXWDkLg==", "dev": true, + "license": "MIT", "dependencies": { "debug": "3.X", "memoizee": "0.4.X", "object-assign": "4.X" } }, - "node_modules/debug-fabulous/node_modules/debug": { + "packages/blockly/node_modules/debug-fabulous/node_modules/debug": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.1" } }, - "node_modules/decamelize": { + "packages/blockly/node_modules/decamelize": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.0.tgz", - "integrity": "sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==", "dev": true, + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -3417,49 +4581,35 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/decimal.js": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", - "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" + "packages/blockly/node_modules/decimal.js": { + "version": "10.5.0", + "license": "MIT" }, - "node_modules/decode-uri-component": { + "packages/blockly/node_modules/decode-uri-component": { "version": "0.2.2", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", - "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10" } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/deep-is": { + "packages/blockly/node_modules/deep-is": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/deepmerge-ts": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.3.tgz", - "integrity": "sha512-qCSH6I0INPxd9Y1VtAiLpnYvz5O//6rCfJXKk0z66Up9/VOSr+1yS8XSKA5IWRxjocFGlzPyaZYe+jxq7OOLtQ==", + "packages/blockly/node_modules/deepmerge-ts": { + "version": "7.1.5", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=16.0.0" } }, - "node_modules/degenerator": { + "packages/blockly/node_modules/degenerator": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", "dev": true, + "license": "MIT", "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", @@ -3469,55 +4619,47 @@ "node": ">= 14" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/dequal": { + "packages/blockly/node_modules/dequal": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/detect-file": { + "packages/blockly/node_modules/detect-file": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", - "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/detect-newline": { + "packages/blockly/node_modules/detect-newline": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", - "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "packages/blockly/node_modules/devtools-protocol": { + "version": "0.0.1495869", "dev": true, + "license": "BSD-3-Clause" + }, + "packages/blockly/node_modules/diff": { + "version": "7.0.0", + "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } }, - "node_modules/dom-serializer": { + "packages/blockly/node_modules/dom-serializer": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dev": true, + "license": "MIT", "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", @@ -3527,23 +4669,21 @@ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, - "node_modules/domelementtype": { + "packages/blockly/node_modules/domelementtype": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "dev": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/fb55" } - ] + ], + "license": "BSD-2-Clause" }, - "node_modules/domhandler": { + "packages/blockly/node_modules/domhandler": { "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.3.0" }, @@ -3554,11 +4694,10 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "node_modules/domutils": { + "packages/blockly/node_modules/domutils": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", - "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", @@ -3568,11 +4707,23 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "node_modules/each-props": { + "packages/blockly/node_modules/dunder-proto": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "packages/blockly/node_modules/each-props": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/each-props/-/each-props-3.0.0.tgz", - "integrity": "sha512-IYf1hpuWrdzse/s/YJOrFmU15lyhSzxelNVAHTEG3DtP4QsLTWZUzcUL3HMXmKQxXpa4EIrBPpwRgj0aehdvAw==", "dev": true, + "license": "MIT", "dependencies": { "is-plain-object": "^5.0.0", "object.defaults": "^1.1.0" @@ -3581,18 +4732,15 @@ "node": ">= 10.13.0" } }, - "node_modules/eastasianwidth": { + "packages/blockly/node_modules/eastasianwidth": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true, "license": "MIT" }, - "node_modules/edge-paths": { + "packages/blockly/node_modules/edge-paths": { "version": "3.0.5", - "resolved": "https://registry.npmjs.org/edge-paths/-/edge-paths-3.0.5.tgz", - "integrity": "sha512-sB7vSrDnFa4ezWQk9nZ/n0FdpdUuC6R1EOrlU3DL+bovcNFK28rqu2emmAUjujYEJTWIgQGqgVVWUZXMnc8iWg==", "dev": true, + "license": "MIT", "dependencies": { "@types/which": "^2.0.1", "which": "^2.0.2" @@ -3604,12 +4752,11 @@ "url": "https://github.com/sponsors/shirshak55" } }, - "node_modules/edgedriver": { + "packages/blockly/node_modules/edgedriver": { "version": "6.1.1", - "resolved": "https://registry.npmjs.org/edgedriver/-/edgedriver-6.1.1.tgz", - "integrity": "sha512-/dM/PoBf22Xg3yypMWkmRQrBKEnSyNaZ7wHGCT9+qqT14izwtFT+QvdR89rjNkMfXwW+bSFoqOfbcvM+2Cyc7w==", "dev": true, "hasInstallScript": true, + "license": "MIT", "dependencies": { "@wdio/logger": "^9.1.3", "@zip.js/zip.js": "^2.7.53", @@ -3628,20 +4775,18 @@ "node": ">=18.0.0" } }, - "node_modules/edgedriver/node_modules/isexe": { + "packages/blockly/node_modules/edgedriver/node_modules/isexe": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "dev": true, + "license": "ISC", "engines": { "node": ">=16" } }, - "node_modules/edgedriver/node_modules/which": { + "packages/blockly/node_modules/edgedriver/node_modules/which": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^3.1.1" }, @@ -3652,17 +4797,10 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/encoding-sniffer": { + "packages/blockly/node_modules/encoding-sniffer": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", - "integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==", "dev": true, + "license": "MIT", "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" @@ -3671,11 +4809,10 @@ "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" } }, - "node_modules/encoding-sniffer/node_modules/whatwg-encoding": { + "packages/blockly/node_modules/encoding-sniffer/node_modules/whatwg-encoding": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", "dev": true, + "license": "MIT", "dependencies": { "iconv-lite": "0.6.3" }, @@ -3683,19 +4820,17 @@ "node": ">=18" } }, - "node_modules/end-of-stream": { + "packages/blockly/node_modules/end-of-stream": { "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "dev": true, + "license": "MIT", "dependencies": { "once": "^1.4.0" } }, - "node_modules/entities": { + "packages/blockly/node_modules/entities": { "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", "engines": { "node": ">=0.12" }, @@ -3703,49 +4838,66 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/es-module-lexer": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", - "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", - "dev": true + "packages/blockly/node_modules/es-define-property": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "packages/blockly/node_modules/es-errors": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "packages/blockly/node_modules/es-object-atoms": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } }, - "node_modules/es5-ext": { + "packages/blockly/node_modules/es5-ext": { "version": "0.10.53", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", - "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", "dev": true, + "license": "ISC", "dependencies": { "es6-iterator": "~2.0.3", "es6-symbol": "~3.1.3", "next-tick": "~1.0.0" } }, - "node_modules/es6-iterator": { + "packages/blockly/node_modules/es6-iterator": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", "dev": true, + "license": "MIT", "dependencies": { "d": "1", "es5-ext": "^0.10.35", "es6-symbol": "^3.1.1" } }, - "node_modules/es6-symbol": { + "packages/blockly/node_modules/es6-symbol": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", - "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", "dev": true, + "license": "ISC", "dependencies": { "d": "^1.0.1", "ext": "^1.1.2" } }, - "node_modules/es6-weak-map": { + "packages/blockly/node_modules/es6-weak-map": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", - "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", "dev": true, + "license": "ISC", "dependencies": { "d": "1", "es5-ext": "^0.10.46", @@ -3753,20 +4905,10 @@ "es6-symbol": "^3.1.1" } }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { + "packages/blockly/node_modules/escape-string-regexp": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -3774,11 +4916,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/escodegen": { + "packages/blockly/node_modules/escodegen": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", @@ -3795,32 +4936,31 @@ "source-map": "~0.6.1" } }, - "node_modules/escodegen/node_modules/source-map": { + "packages/blockly/node_modules/escodegen/node_modules/source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, + "license": "BSD-3-Clause", "optional": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/eslint": { - "version": "9.17.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.17.0.tgz", - "integrity": "sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==", + "packages/blockly/node_modules/eslint": { + "version": "9.36.0", "dev": true, + "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.9.0", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.17.0", - "@eslint/plugin-kit": "^0.2.3", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.36.0", + "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.1", + "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -3828,9 +4968,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.2.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -3864,11 +5004,10 @@ } } }, - "node_modules/eslint-config-google": { + "packages/blockly/node_modules/eslint-config-google": { "version": "0.14.0", - "resolved": "https://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.14.0.tgz", - "integrity": "sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=0.10.0" }, @@ -3876,48 +5015,47 @@ "eslint": ">=5.16.0" } }, - "node_modules/eslint-config-prettier": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", - "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "packages/blockly/node_modules/eslint-config-prettier": { + "version": "10.1.8", "dev": true, + "license": "MIT", "bin": { "eslint-config-prettier": "bin/cli.js" }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, "peerDependencies": { "eslint": ">=7.0.0" } }, - "node_modules/eslint-plugin-jsdoc": { - "version": "50.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.6.0.tgz", - "integrity": "sha512-tCNp4fR79Le3dYTPB0dKEv7yFyvGkUCa+Z3yuTrrNGGOxBlXo9Pn0PEgroOZikUQOGjxoGMVKNjrOHcYEdfszg==", + "packages/blockly/node_modules/eslint-plugin-jsdoc": { + "version": "52.0.2", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@es-joy/jsdoccomment": "~0.49.0", + "@es-joy/jsdoccomment": "~0.52.0", "are-docs-informative": "^0.0.2", "comment-parser": "1.4.1", - "debug": "^4.3.6", + "debug": "^4.4.1", "escape-string-regexp": "^4.0.0", - "espree": "^10.1.0", + "espree": "^10.4.0", "esquery": "^1.6.0", - "parse-imports": "^2.1.1", - "semver": "^7.6.3", - "spdx-expression-parse": "^4.0.0", - "synckit": "^0.9.1" + "parse-imports-exports": "^0.2.4", + "semver": "^7.7.2", + "spdx-expression-parse": "^4.0.0" }, "engines": { - "node": ">=18" + "node": ">=20.11.0" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" } }, - "node_modules/eslint-plugin-jsdoc/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "packages/blockly/node_modules/eslint-plugin-jsdoc/node_modules/semver": { + "version": "7.7.2", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -3925,25 +5063,45 @@ "node": ">=10" } }, - "node_modules/eslint-plugin-jsdoc/node_modules/spdx-expression-parse": { + "packages/blockly/node_modules/eslint-plugin-jsdoc/node_modules/spdx-expression-parse": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", - "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", "dev": true, + "license": "MIT", "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, - "node_modules/eslint-plugin-prettier": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.3.tgz", - "integrity": "sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw==", + "packages/blockly/node_modules/eslint-plugin-mocha": { + "version": "11.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.1", + "globals": "^15.14.0" + }, + "peerDependencies": { + "eslint": ">=9.0.0" + } + }, + "packages/blockly/node_modules/eslint-plugin-mocha/node_modules/globals": { + "version": "15.15.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/blockly/node_modules/eslint-plugin-prettier": { + "version": "5.5.4", "dev": true, "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.9.1" + "synckit": "^0.11.7" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -3954,7 +5112,7 @@ "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", - "eslint-config-prettier": "*", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "peerDependenciesMeta": { @@ -3966,11 +5124,10 @@ } } }, - "node_modules/eslint-scope": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", - "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "packages/blockly/node_modules/eslint-scope": { + "version": "8.4.0", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -3982,11 +5139,10 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-visitor-keys": { + "packages/blockly/node_modules/eslint-visitor-keys": { "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -3994,11 +5150,25 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "packages/blockly/node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "packages/blockly/node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -4006,11 +5176,10 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/glob-parent": { + "packages/blockly/node_modules/eslint/node_modules/glob-parent": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.3" }, @@ -4018,15 +5187,19 @@ "node": ">=10.13.0" } }, - "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "packages/blockly/node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "packages/blockly/node_modules/espree": { + "version": "10.4.0", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4035,11 +5208,10 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "packages/blockly/node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -4047,11 +5219,10 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/esprima": { + "packages/blockly/node_modules/esprima": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true, + "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -4060,11 +5231,10 @@ "node": ">=4" } }, - "node_modules/esquery": { + "packages/blockly/node_modules/esquery": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" }, @@ -4072,11 +5242,10 @@ "node": ">=0.10" } }, - "node_modules/esrecurse": { + "packages/blockly/node_modules/esrecurse": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -4084,63 +5253,56 @@ "node": ">=4.0" } }, - "node_modules/estraverse": { + "packages/blockly/node_modules/estraverse": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } }, - "node_modules/esutils": { + "packages/blockly/node_modules/esutils": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } }, - "node_modules/event-emitter": { + "packages/blockly/node_modules/event-emitter": { "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", "dev": true, + "license": "MIT", "dependencies": { "d": "1", "es5-ext": "~0.10.14" } }, - "node_modules/event-target-shim": { + "packages/blockly/node_modules/event-target-shim": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/eventemitter3": { + "packages/blockly/node_modules/eventemitter3": { "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/events": { + "packages/blockly/node_modules/events": { "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.x" } }, - "node_modules/expand-tilde": { + "packages/blockly/node_modules/expand-tilde": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", - "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", "dev": true, + "license": "MIT", "dependencies": { "homedir-polyfill": "^1.0.1" }, @@ -4148,32 +5310,28 @@ "node": ">=0.10.0" } }, - "node_modules/ext": { + "packages/blockly/node_modules/ext": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.6.0.tgz", - "integrity": "sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg==", "dev": true, + "license": "ISC", "dependencies": { "type": "^2.5.0" } }, - "node_modules/ext/node_modules/type": { + "packages/blockly/node_modules/ext/node_modules/type": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/type/-/type-2.5.0.tgz", - "integrity": "sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw==", - "dev": true + "dev": true, + "license": "ISC" }, - "node_modules/extend": { + "packages/blockly/node_modules/extend": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/extend-shallow": { + "packages/blockly/node_modules/extend-shallow": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", "dev": true, + "license": "MIT", "dependencies": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" @@ -4182,11 +5340,10 @@ "node": ">=0.10.0" } }, - "node_modules/extract-zip": { + "packages/blockly/node_modules/extract-zip": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", @@ -4202,11 +5359,10 @@ "@types/yauzl": "^2.9.1" } }, - "node_modules/fancy-log": { + "packages/blockly/node_modules/fancy-log": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz", - "integrity": "sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw==", "dev": true, + "license": "MIT", "dependencies": { "ansi-gray": "^0.1.1", "color-support": "^1.1.3", @@ -4217,28 +5373,13 @@ "node": ">= 0.10" } }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-diff": { + "packages/blockly/node_modules/fast-diff": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", - "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", - "dev": true - }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, - "node_modules/fast-glob": { + "packages/blockly/node_modules/fast-glob": { "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, "license": "MIT", "dependencies": { @@ -4252,71 +5393,59 @@ "node": ">=8.6.0" } }, - "node_modules/fast-json-stable-stringify": { + "packages/blockly/node_modules/fast-json-stable-stringify": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/fast-levenshtein": { + "packages/blockly/node_modules/fast-levenshtein": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/fast-xml-parser": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.1.tgz", - "integrity": "sha512-y655CeyUQ+jj7KBbYMc4FG01V8ZQqjN+gDYGJ50RtfsUB8iG9AmwmwoAgeKLJdmueKKMrH1RJ7yXHTSoczdv5w==", + "packages/blockly/node_modules/fast-xml-parser": { + "version": "4.5.3", "dev": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" - }, - { - "type": "paypal", - "url": "https://paypal.me/naturalintelligence" } ], + "license": "MIT", "dependencies": { - "strnum": "^1.0.5" + "strnum": "^1.1.1" }, "bin": { "fxparser": "src/cli/cli.js" } }, - "node_modules/fastest-levenshtein": { + "packages/blockly/node_modules/fastest-levenshtein": { "version": "1.0.16", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", - "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4.9.1" } }, - "node_modules/fastq": { + "packages/blockly/node_modules/fastq": { "version": "1.13.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", - "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", "dev": true, + "license": "ISC", "dependencies": { "reusify": "^1.0.4" } }, - "node_modules/fd-slicer": { + "packages/blockly/node_modules/fd-slicer": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", "dev": true, + "license": "MIT", "dependencies": { "pend": "~1.2.0" } }, - "node_modules/fetch-blob": { + "packages/blockly/node_modules/fetch-blob": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", "dev": true, "funding": [ { @@ -4328,6 +5457,7 @@ "url": "https://paypal.me/jimmywarting" } ], + "license": "MIT", "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" @@ -4336,11 +5466,10 @@ "node": "^12.20 || >= 14.13" } }, - "node_modules/file-entry-cache": { + "packages/blockly/node_modules/file-entry-cache": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, + "license": "MIT", "dependencies": { "flat-cache": "^4.0.0" }, @@ -4348,11 +5477,10 @@ "node": ">=16.0.0" } }, - "node_modules/fill-range": { + "packages/blockly/node_modules/fill-range": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -4360,11 +5488,10 @@ "node": ">=8" } }, - "node_modules/find-up": { + "packages/blockly/node_modules/find-up": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -4376,20 +5503,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/find-yarn-workspace-root": { + "packages/blockly/node_modules/find-yarn-workspace-root": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", - "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { "micromatch": "^4.0.2" } }, - "node_modules/findup-sync": { + "packages/blockly/node_modules/findup-sync": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz", - "integrity": "sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==", "dev": true, + "license": "MIT", "dependencies": { "detect-file": "^1.0.0", "is-glob": "^4.0.3", @@ -4400,11 +5525,10 @@ "node": ">= 10.13.0" } }, - "node_modules/fined": { + "packages/blockly/node_modules/fined": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fined/-/fined-2.0.0.tgz", - "integrity": "sha512-OFRzsL6ZMHz5s0JrsEr+TpdGNCtrVtnuG3x1yzGNiQHT0yaDnXAj8V/lWcpJVrnoDpcwXcASxAZYbuXda2Y82A==", "dev": true, + "license": "MIT", "dependencies": { "expand-tilde": "^2.0.2", "is-plain-object": "^5.0.0", @@ -4416,29 +5540,26 @@ "node": ">= 10.13.0" } }, - "node_modules/flagged-respawn": { + "packages/blockly/node_modules/flagged-respawn": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-2.0.0.tgz", - "integrity": "sha512-Gq/a6YCi8zexmGHMuJwahTGzXlAZAOsbCVKduWXC6TlLCjjFRlExMJc4GC2NYPYZ0r/brw9P7CpRgQmlPVeOoA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10.13.0" } }, - "node_modules/flat": { + "packages/blockly/node_modules/flat": { "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", "dev": true, + "license": "BSD-3-Clause", "bin": { "flat": "cli.js" } }, - "node_modules/flat-cache": { + "packages/blockly/node_modules/flat-cache": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, + "license": "MIT", "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" @@ -4447,16 +5568,13 @@ "node": ">=16" } }, - "node_modules/flatted": { + "packages/blockly/node_modules/flatted": { "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", - "dev": true + "dev": true, + "license": "ISC" }, - "node_modules/follow-redirects": { + "packages/blockly/node_modules/follow-redirects": { "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "dev": true, "funding": [ { @@ -4464,6 +5582,7 @@ "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -4473,20 +5592,18 @@ } } }, - "node_modules/for-in": { + "packages/blockly/node_modules/for-in": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/for-own": { + "packages/blockly/node_modules/for-own": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", - "integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==", "dev": true, + "license": "MIT", "dependencies": { "for-in": "^1.0.1" }, @@ -4494,13 +5611,12 @@ "node": ">=0.10.0" } }, - "node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "packages/blockly/node_modules/foreground-child": { + "version": "3.3.1", "dev": true, + "license": "ISC", "dependencies": { - "cross-spawn": "^7.0.0", + "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" }, "engines": { @@ -4510,24 +5626,10 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/formdata-polyfill": { + "packages/blockly/node_modules/formdata-polyfill": { "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", "dev": true, + "license": "MIT", "dependencies": { "fetch-blob": "^3.1.2" }, @@ -4535,11 +5637,10 @@ "node": ">=12.20.0" } }, - "node_modules/fs-extra": { + "packages/blockly/node_modules/fs-extra": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, + "license": "MIT", "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -4549,29 +5650,26 @@ "node": ">=6 <7 || >=8" } }, - "node_modules/fs-extra/node_modules/jsonfile": { + "packages/blockly/node_modules/fs-extra/node_modules/jsonfile": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, + "license": "MIT", "optionalDependencies": { "graceful-fs": "^4.1.6" } }, - "node_modules/fs-extra/node_modules/universalify": { + "packages/blockly/node_modules/fs-extra/node_modules/universalify": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4.0.0" } }, - "node_modules/fs-mkdirp-stream": { + "packages/blockly/node_modules/fs-mkdirp-stream": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-2.0.1.tgz", - "integrity": "sha512-UTOY+59K6IA94tec8Wjqm0FSh5OVudGNB0NL/P6fB3HiE3bYOY3VYBGijsnOHNkQSwC1FKkU77pmq7xp9CskLw==", "dev": true, + "license": "MIT", "dependencies": { "graceful-fs": "^4.2.8", "streamx": "^2.12.0" @@ -4580,27 +5678,31 @@ "node": ">=10.13.0" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "packages/blockly/node_modules/fsevents": { + "version": "2.3.3", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } }, - "node_modules/function-bind": { + "packages/blockly/node_modules/function-bind": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/geckodriver": { + "packages/blockly/node_modules/geckodriver": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/geckodriver/-/geckodriver-5.0.0.tgz", - "integrity": "sha512-vn7TtQ3b9VMJtVXsyWtQQl1fyBVFhQy7UvJF96kPuuJ0or5THH496AD3eUyaDD11+EqCxH9t6V+EP9soZQk4YQ==", "dev": true, "hasInstallScript": true, + "license": "MIT", "dependencies": { "@wdio/logger": "^9.1.3", "@zip.js/zip.js": "^2.7.53", @@ -4618,20 +5720,18 @@ "node": ">=18.0.0" } }, - "node_modules/geckodriver/node_modules/isexe": { + "packages/blockly/node_modules/geckodriver/node_modules/isexe": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "dev": true, + "license": "ISC", "engines": { "node": ">=16" } }, - "node_modules/geckodriver/node_modules/which": { + "packages/blockly/node_modules/geckodriver/node_modules/which": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^3.1.1" }, @@ -4642,29 +5742,41 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "packages/blockly/node_modules/get-func-name": { + "version": "2.0.2", "dev": true, + "license": "MIT", "engines": { - "node": "6.* || 8.* || >= 10.*" + "node": "*" } }, - "node_modules/get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "packages/blockly/node_modules/get-intrinsic": { + "version": "1.3.0", "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, "engines": { - "node": "*" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-port": { + "packages/blockly/node_modules/get-port": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.1.0.tgz", - "integrity": "sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==", "dev": true, + "license": "MIT", "engines": { "node": ">=16" }, @@ -4672,11 +5784,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-stream": { + "packages/blockly/node_modules/get-proto": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "packages/blockly/node_modules/get-stream": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "dev": true, + "license": "MIT", "dependencies": { "pump": "^3.0.0" }, @@ -4687,11 +5810,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-uri": { + "packages/blockly/node_modules/get-uri": { "version": "6.0.4", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz", - "integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==", "dev": true, + "license": "MIT", "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", @@ -4701,25 +5823,22 @@ "node": ">= 14" } }, - "node_modules/get-uri/node_modules/data-uri-to-buffer": { + "packages/blockly/node_modules/get-uri/node_modules/data-uri-to-buffer": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 14" } }, - "node_modules/glob": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz", - "integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==", + "packages/blockly/node_modules/glob": { + "version": "11.0.3", "dev": true, "license": "ISC", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^4.0.1", - "minimatch": "^10.0.0", + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" @@ -4734,11 +5853,10 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/glob-parent": { + "packages/blockly/node_modules/glob-parent": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -4746,11 +5864,10 @@ "node": ">= 6" } }, - "node_modules/glob-stream": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-8.0.2.tgz", - "integrity": "sha512-R8z6eTB55t3QeZMmU1C+Gv+t5UnNRkA55c5yo67fAVfxODxieTwsjNG7utxS/73NdP1NbDgCrhVEg2h00y4fFw==", + "packages/blockly/node_modules/glob-stream": { + "version": "8.0.3", "dev": true, + "license": "MIT", "dependencies": { "@gulpjs/to-absolute-glob": "^4.0.0", "anymatch": "^3.1.3", @@ -4765,11 +5882,10 @@ "node": ">=10.13.0" } }, - "node_modules/glob-stream/node_modules/glob-parent": { + "packages/blockly/node_modules/glob-stream/node_modules/glob-parent": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.3" }, @@ -4777,11 +5893,10 @@ "node": ">=10.13.0" } }, - "node_modules/glob-watcher": { + "packages/blockly/node_modules/glob-watcher": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-6.0.0.tgz", - "integrity": "sha512-wGM28Ehmcnk2NqRORXFOTOR064L4imSw3EeOqU5bIwUf62eXGwg89WivH6VMahL8zlQHeodzvHpXplrqzrz3Nw==", "dev": true, + "license": "MIT", "dependencies": { "async-done": "^2.0.0", "chokidar": "^3.5.3" @@ -4790,24 +5905,12 @@ "node": ">= 10.13.0" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "packages/blockly/node_modules/glob/node_modules/minimatch": { + "version": "10.0.3", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { "node": "20 || >=22" @@ -4816,11 +5919,10 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/global-modules": { + "packages/blockly/node_modules/global-modules": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", - "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", "dev": true, + "license": "MIT", "dependencies": { "global-prefix": "^1.0.1", "is-windows": "^1.0.1", @@ -4830,11 +5932,10 @@ "node": ">=0.10.0" } }, - "node_modules/global-prefix": { + "packages/blockly/node_modules/global-prefix": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", - "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", "dev": true, + "license": "MIT", "dependencies": { "expand-tilde": "^2.0.2", "homedir-polyfill": "^1.0.1", @@ -4846,11 +5947,10 @@ "node": ">=0.10.0" } }, - "node_modules/global-prefix/node_modules/which": { + "packages/blockly/node_modules/global-prefix/node_modules/which": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -4858,10 +5958,8 @@ "which": "bin/which" } }, - "node_modules/globals": { - "version": "15.14.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz", - "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==", + "packages/blockly/node_modules/globals": { + "version": "16.4.0", "dev": true, "license": "MIT", "engines": { @@ -4871,11 +5969,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/glogg": { + "packages/blockly/node_modules/glogg": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/glogg/-/glogg-2.2.0.tgz", - "integrity": "sha512-eWv1ds/zAlz+M1ioHsyKJomfY7jbDDPpwSkv14KQj89bycx1nvK5/2Cj/T9g7kzJcX5Bc7Yv22FjfBZS/jl94A==", "dev": true, + "license": "MIT", "dependencies": { "sparkles": "^2.1.0" }, @@ -4883,108 +5980,41 @@ "node": ">= 10.13.0" } }, - "node_modules/google-closure-compiler": { - "version": "20240317.0.0", - "resolved": "https://registry.npmjs.org/google-closure-compiler/-/google-closure-compiler-20240317.0.0.tgz", - "integrity": "sha512-PlC5aU2vwsypKbxyFNXOW4psDZfhDoOr2dCwuo8VcgQji+HVIgRi2lviO66x2SfTi0ilm3kI6rq/RSdOMFczcQ==", + "packages/blockly/node_modules/gopd": { + "version": "1.2.0", "dev": true, - "dependencies": { - "chalk": "4.x", - "google-closure-compiler-java": "^20240317.0.0", - "minimist": "1.x", - "vinyl": "2.x", - "vinyl-sourcemaps-apply": "^0.2.0" - }, - "bin": { - "google-closure-compiler": "cli.js" - }, + "license": "MIT", "engines": { - "node": ">=10" + "node": ">= 0.4" }, - "optionalDependencies": { - "google-closure-compiler-linux": "^20240317.0.0", - "google-closure-compiler-osx": "^20240317.0.0", - "google-closure-compiler-windows": "^20240317.0.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/google-closure-compiler-java": { - "version": "20240317.0.0", - "resolved": "https://registry.npmjs.org/google-closure-compiler-java/-/google-closure-compiler-java-20240317.0.0.tgz", - "integrity": "sha512-oWURPChjcCrVfiQOuVtpSoUJVvtOYo41JGEQ2qtArsTGmk/DpWh40vS6hitwKRM/0YzJX/jYUuyt9ibuXXJKmg==", - "dev": true - }, - "node_modules/google-closure-compiler-linux": { - "version": "20240317.0.0", - "resolved": "https://registry.npmjs.org/google-closure-compiler-linux/-/google-closure-compiler-linux-20240317.0.0.tgz", - "integrity": "sha512-dYLtcbbJdbbBS0lTy9SzySdVv/aGkpyTekQiW4ADhT/i1p1b4r0wQTKj6kpVVmFvbZ6t9tW/jbXc9EXXNUahZw==", - "cpu": [ - "x32", - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/google-closure-compiler-osx": { - "version": "20240317.0.0", - "resolved": "https://registry.npmjs.org/google-closure-compiler-osx/-/google-closure-compiler-osx-20240317.0.0.tgz", - "integrity": "sha512-0mABwjD4HP11rikFd8JRIb9OgPqn9h3o3wS0otufMfmbwS7zRpnnoJkunifhORl3VoR1gFm6vcTC9YziTEFdOw==", - "cpu": [ - "x32", - "x64", - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/google-closure-compiler-windows": { - "version": "20240317.0.0", - "resolved": "https://registry.npmjs.org/google-closure-compiler-windows/-/google-closure-compiler-windows-20240317.0.0.tgz", - "integrity": "sha512-fTueVFzNOWURFlXZmrFkAB7yA+jzpA2TeDOYeBEFwVlVGHwi8PV3Q9vCIWlbkE8wLpukKEg5wfRHYrLwVPINCA==", - "cpu": [ - "x32", - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/graceful-fs": { + "packages/blockly/node_modules/graceful-fs": { "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "dev": true, + "license": "ISC" }, - "node_modules/grapheme-splitter": { + "packages/blockly/node_modules/grapheme-splitter": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/graphemer": { + "packages/blockly/node_modules/graphemer": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true, "license": "MIT" }, - "node_modules/gulp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/gulp/-/gulp-5.0.0.tgz", - "integrity": "sha512-S8Z8066SSileaYw1S2N1I64IUc/myI2bqe2ihOBzO6+nKpvNSg7ZcWJt/AwF8LC/NVN+/QZ560Cb/5OPsyhkhg==", + "packages/blockly/node_modules/gulp": { + "version": "5.0.1", "dev": true, + "license": "MIT", "dependencies": { "glob-watcher": "^6.0.0", - "gulp-cli": "^3.0.0", + "gulp-cli": "^3.1.0", "undertaker": "^2.0.0", - "vinyl-fs": "^4.0.0" + "vinyl-fs": "^4.0.2" }, "bin": { "gulp": "bin/gulp.js" @@ -4993,18 +6023,17 @@ "node": ">=10.13.0" } }, - "node_modules/gulp-cli": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-3.0.0.tgz", - "integrity": "sha512-RtMIitkT8DEMZZygHK2vEuLPqLPAFB4sntSxg4NoDta7ciwGZ18l7JuhCTiS5deOJi2IoK0btE+hs6R4sfj7AA==", + "packages/blockly/node_modules/gulp-cli": { + "version": "3.1.0", "dev": true, + "license": "MIT", "dependencies": { "@gulpjs/messages": "^1.1.0", "chalk": "^4.1.2", "copy-props": "^4.0.0", "gulplog": "^2.2.0", "interpret": "^3.1.1", - "liftoff": "^5.0.0", + "liftoff": "^5.0.1", "mute-stdout": "^2.0.0", "replace-homedir": "^2.0.0", "semver-greatest-satisfied-range": "^2.0.0", @@ -5019,11 +6048,10 @@ "node": ">=10.13.0" } }, - "node_modules/gulp-cli/node_modules/yargs": { + "packages/blockly/node_modules/gulp-cli/node_modules/yargs": { "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, + "license": "MIT", "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", @@ -5037,20 +6065,18 @@ "node": ">=10" } }, - "node_modules/gulp-cli/node_modules/yargs-parser": { + "packages/blockly/node_modules/gulp-cli/node_modules/yargs-parser": { "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } }, - "node_modules/gulp-concat": { + "packages/blockly/node_modules/gulp-concat": { "version": "2.6.1", - "resolved": "https://registry.npmjs.org/gulp-concat/-/gulp-concat-2.6.1.tgz", - "integrity": "sha1-Yz0WyV2IUEYorQJmVmPO5aR5M1M=", "dev": true, + "license": "MIT", "dependencies": { "concat-with-sourcemaps": "^1.0.0", "through2": "^2.0.0", @@ -5060,11 +6086,10 @@ "node": ">= 0.10" } }, - "node_modules/gulp-concat/node_modules/readable-stream": { + "packages/blockly/node_modules/gulp-concat/node_modules/readable-stream": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, + "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -5075,21 +6100,19 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/gulp-concat/node_modules/through2": { + "packages/blockly/node_modules/gulp-concat/node_modules/through2": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", "dev": true, + "license": "MIT", "dependencies": { "readable-stream": "~2.3.6", "xtend": "~4.0.1" } }, - "node_modules/gulp-gzip": { + "packages/blockly/node_modules/gulp-gzip": { "version": "1.4.2", - "resolved": "https://registry.npmjs.org/gulp-gzip/-/gulp-gzip-1.4.2.tgz", - "integrity": "sha512-ZIxfkUwk2XmZPTT9pPHrHUQlZMyp9nPhg2sfoeN27mBGpi7OaHnOD+WCN41NXjfJQ69lV1nQ9LLm1hYxx4h3UQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-colors": "^1.0.1", "bytes": "^3.0.0", @@ -5102,11 +6125,10 @@ "node": ">= 0.10.0" } }, - "node_modules/gulp-gzip/node_modules/ansi-colors": { + "packages/blockly/node_modules/gulp-gzip/node_modules/ansi-colors": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", - "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-wrap": "^0.1.0" }, @@ -5114,11 +6136,10 @@ "node": ">=0.10.0" } }, - "node_modules/gulp-gzip/node_modules/readable-stream": { + "packages/blockly/node_modules/gulp-gzip/node_modules/readable-stream": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, + "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -5129,21 +6150,19 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/gulp-gzip/node_modules/through2": { + "packages/blockly/node_modules/gulp-gzip/node_modules/through2": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", "dev": true, + "license": "MIT", "dependencies": { "readable-stream": "~2.3.6", "xtend": "~4.0.1" } }, - "node_modules/gulp-header": { + "packages/blockly/node_modules/gulp-header": { "version": "2.0.9", - "resolved": "https://registry.npmjs.org/gulp-header/-/gulp-header-2.0.9.tgz", - "integrity": "sha512-LMGiBx+qH8giwrOuuZXSGvswcIUh0OiioNkUpLhNyvaC6/Ga8X6cfAeme2L5PqsbXMhL8o8b/OmVqIQdxprhcQ==", "dev": true, + "license": "MIT", "dependencies": { "concat-with-sourcemaps": "^1.1.0", "lodash.template": "^4.5.0", @@ -5151,11 +6170,10 @@ "through2": "^2.0.0" } }, - "node_modules/gulp-header/node_modules/readable-stream": { + "packages/blockly/node_modules/gulp-header/node_modules/readable-stream": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, + "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -5166,37 +6184,33 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/gulp-header/node_modules/through2": { + "packages/blockly/node_modules/gulp-header/node_modules/through2": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", "dev": true, + "license": "MIT", "dependencies": { "readable-stream": "~2.3.6", "xtend": "~4.0.1" } }, - "node_modules/gulp-insert": { + "packages/blockly/node_modules/gulp-insert": { "version": "0.5.0", - "resolved": "https://registry.npmjs.org/gulp-insert/-/gulp-insert-0.5.0.tgz", - "integrity": "sha1-MjE/E+SiPPWsylzl8MCAkjx3hgI=", "dev": true, + "license": "MIT", "dependencies": { "readable-stream": "^1.0.26-4", "streamqueue": "0.0.6" } }, - "node_modules/gulp-insert/node_modules/isarray": { + "packages/blockly/node_modules/gulp-insert/node_modules/isarray": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/gulp-insert/node_modules/readable-stream": { + "packages/blockly/node_modules/gulp-insert/node_modules/readable-stream": { "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", "dev": true, + "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", @@ -5204,26 +6218,23 @@ "string_decoder": "~0.10.x" } }, - "node_modules/gulp-insert/node_modules/string_decoder": { + "packages/blockly/node_modules/gulp-insert/node_modules/string_decoder": { "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/gulp-rename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/gulp-rename/-/gulp-rename-2.0.0.tgz", - "integrity": "sha512-97Vba4KBzbYmR5VBs9mWmK+HwIf5mj+/zioxfZhOKeXtx5ZjBk57KFlePf5nxq9QsTtFl0ejnHE3zTC9MHXqyQ==", + "packages/blockly/node_modules/gulp-rename": { + "version": "2.1.0", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, - "node_modules/gulp-replace": { + "packages/blockly/node_modules/gulp-replace": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/gulp-replace/-/gulp-replace-1.1.4.tgz", - "integrity": "sha512-SVSF7ikuWKhpAW4l4wapAqPPSToJoiNKsbDoUnRrSgwZHH7lH8pbPeQj1aOVYQrbZKhfSVBxVW+Py7vtulRktw==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*", "@types/vinyl": "^2.0.4", @@ -5235,17 +6246,15 @@ "node": ">=10" } }, - "node_modules/gulp-series": { + "packages/blockly/node_modules/gulp-series": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/gulp-series/-/gulp-series-1.0.2.tgz", - "integrity": "sha1-gWGZA1AXh13QDUiIklBP659jCgs=", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/gulp-shell": { + "packages/blockly/node_modules/gulp-shell": { "version": "0.8.0", - "resolved": "https://registry.npmjs.org/gulp-shell/-/gulp-shell-0.8.0.tgz", - "integrity": "sha512-wHNCgmqbWkk1c6Gc2dOL5SprcoeujQdeepICwfQRo91DIylTE7a794VEE+leq3cE2YDoiS5ulvRfKVIEMazcTQ==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^3.0.0", "fancy-log": "^1.3.3", @@ -5258,11 +6267,10 @@ "node": ">=10.0.0" } }, - "node_modules/gulp-shell/node_modules/chalk": { + "packages/blockly/node_modules/gulp-shell/node_modules/chalk": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -5271,21 +6279,19 @@ "node": ">=8" } }, - "node_modules/gulp-shell/node_modules/through2": { + "packages/blockly/node_modules/gulp-shell/node_modules/through2": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", - "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", "dev": true, + "license": "MIT", "dependencies": { "inherits": "^2.0.4", "readable-stream": "2 || 3" } }, - "node_modules/gulp-sourcemaps": { + "packages/blockly/node_modules/gulp-sourcemaps": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-3.0.0.tgz", - "integrity": "sha512-RqvUckJkuYqy4VaIH60RMal4ZtG0IbQ6PXMNkNsshEGJ9cldUPRb/YCgboYae+CLAs1HQNb4ADTKCx65HInquQ==", "dev": true, + "license": "ISC", "dependencies": { "@gulp-sourcemaps/identity-map": "^2.0.1", "@gulp-sourcemaps/map-sources": "^1.0.0", @@ -5303,11 +6309,10 @@ "node": ">= 6" } }, - "node_modules/gulp-sourcemaps/node_modules/acorn": { + "packages/blockly/node_modules/gulp-sourcemaps/node_modules/acorn": { "version": "6.4.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", - "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -5315,11 +6320,10 @@ "node": ">=0.4.0" } }, - "node_modules/gulp-sourcemaps/node_modules/readable-stream": { + "packages/blockly/node_modules/gulp-sourcemaps/node_modules/readable-stream": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, + "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -5330,41 +6334,37 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/gulp-sourcemaps/node_modules/source-map": { + "packages/blockly/node_modules/gulp-sourcemaps/node_modules/source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, - "node_modules/gulp-sourcemaps/node_modules/through2": { + "packages/blockly/node_modules/gulp-sourcemaps/node_modules/through2": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", "dev": true, + "license": "MIT", "dependencies": { "readable-stream": "~2.3.6", "xtend": "~4.0.1" } }, - "node_modules/gulp-umd": { + "packages/blockly/node_modules/gulp-umd": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/gulp-umd/-/gulp-umd-2.0.0.tgz", - "integrity": "sha512-zA0RDIITdOwpVUBQ6vy2R+iCsTXwDImPnWreNBmVJQAg3nDGefowV7KYwWoIeEVoxyHZT2CR50nEF6ovUh5/2A==", "dev": true, + "license": "MIT", "dependencies": { "concat-stream": "^1.6.2", "lodash.template": "^4.4.0", "through2": "^2.0.3" } }, - "node_modules/gulp-umd/node_modules/readable-stream": { + "packages/blockly/node_modules/gulp-umd/node_modules/readable-stream": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, + "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -5375,21 +6375,19 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/gulp-umd/node_modules/through2": { + "packages/blockly/node_modules/gulp-umd/node_modules/through2": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", "dev": true, + "license": "MIT", "dependencies": { "readable-stream": "~2.3.6", "xtend": "~4.0.1" } }, - "node_modules/gulplog": { + "packages/blockly/node_modules/gulplog": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-2.2.0.tgz", - "integrity": "sha512-V2FaKiOhpR3DRXZuYdRLn/qiY0yI5XmqbTKrYbdemJ+xOh2d2MOweI/XFgMzd/9+1twdvMwllnZbWZNJ+BOm4A==", "dev": true, + "license": "MIT", "dependencies": { "glogg": "^2.2.0" }, @@ -5397,20 +6395,29 @@ "node": ">= 10.13.0" } }, - "node_modules/has-flag": { + "packages/blockly/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/hasown": { + "packages/blockly/node_modules/has-symbols": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "packages/blockly/node_modules/hasown": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -5418,20 +6425,18 @@ "node": ">= 0.4" } }, - "node_modules/he": { + "packages/blockly/node_modules/he": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true, + "license": "MIT", "bin": { "he": "bin/he" } }, - "node_modules/homedir-polyfill": { + "packages/blockly/node_modules/homedir-polyfill": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", - "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", "dev": true, + "license": "MIT", "dependencies": { "parse-passwd": "^1.0.0" }, @@ -5439,11 +6444,10 @@ "node": ">=0.10.0" } }, - "node_modules/html-encoding-sniffer": { + "packages/blockly/node_modules/html-encoding-sniffer": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", - "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", "dev": true, + "license": "MIT", "dependencies": { "whatwg-encoding": "^2.0.0" }, @@ -5451,16 +6455,13 @@ "node": ">=12" } }, - "node_modules/htmlfy": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/htmlfy/-/htmlfy-0.3.2.tgz", - "integrity": "sha512-FsxzfpeDYRqn1emox9VpxMPfGjADoUmmup8D604q497R0VNxiXs4ZZTN2QzkaMA5C9aHGUoe1iQRVSm+HK9xuA==", - "dev": true + "packages/blockly/node_modules/htmlfy": { + "version": "0.6.7", + "dev": true, + "license": "MIT" }, - "node_modules/htmlparser2": { + "packages/blockly/node_modules/htmlparser2": { "version": "9.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", - "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", "dev": true, "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", @@ -5469,6 +6470,7 @@ "url": "https://github.com/sponsors/fb55" } ], + "license": "MIT", "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", @@ -5476,11 +6478,10 @@ "entities": "^4.5.0" } }, - "node_modules/http-proxy": { + "packages/blockly/node_modules/http-proxy": { "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", "dev": true, + "license": "MIT", "dependencies": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", @@ -5490,10 +6491,9 @@ "node": ">=8.0.0" } }, - "node_modules/http-proxy-agent": { + "packages/blockly/node_modules/http-proxy-agent": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" @@ -5502,11 +6502,10 @@ "node": ">= 14" } }, - "node_modules/http-server": { + "packages/blockly/node_modules/http-server": { "version": "14.1.1", - "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz", - "integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==", "dev": true, + "license": "MIT", "dependencies": { "basic-auth": "^2.0.1", "chalk": "^4.1.2", @@ -5529,10 +6528,9 @@ "node": ">=12" } }, - "node_modules/https-proxy-agent": { + "packages/blockly/node_modules/https-proxy-agent": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", "dependencies": { "agent-base": "^7.1.2", "debug": "4" @@ -5541,10 +6539,9 @@ "node": ">= 14" } }, - "node_modules/iconv-lite": { + "packages/blockly/node_modules/iconv-lite": { "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -5552,10 +6549,8 @@ "node": ">=0.10.0" } }, - "node_modules/ieee754": { + "packages/blockly/node_modules/ieee754": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "dev": true, "funding": [ { @@ -5570,103 +6565,60 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "BSD-3-Clause" }, - "node_modules/ignore": { + "packages/blockly/node_modules/ignore": { "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } }, - "node_modules/immediate": { + "packages/blockly/node_modules/immediate": { "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "dev": true - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "license": "MIT" }, - "node_modules/import-lazy": { + "packages/blockly/node_modules/import-lazy": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", - "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/import-meta-resolve": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", - "integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/imurmurhash": { + "packages/blockly/node_modules/imurmurhash": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.19" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { + "packages/blockly/node_modules/inherits": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "dev": true, + "license": "ISC" }, - "node_modules/ini": { + "packages/blockly/node_modules/ini": { "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true + "dev": true, + "license": "ISC" }, - "node_modules/interpret": { + "packages/blockly/node_modules/interpret": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", - "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.13.0" } }, - "node_modules/ip-address": { + "packages/blockly/node_modules/ip-address": { "version": "9.0.5", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", - "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", "dev": true, + "license": "MIT", "dependencies": { "jsbn": "1.1.0", "sprintf-js": "^1.1.3" @@ -5675,17 +6627,15 @@ "node": ">= 12" } }, - "node_modules/ip-address/node_modules/sprintf-js": { + "packages/blockly/node_modules/ip-address/node_modules/sprintf-js": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, - "node_modules/is-absolute": { + "packages/blockly/node_modules/is-absolute": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", - "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", "dev": true, + "license": "MIT", "dependencies": { "is-relative": "^1.0.0", "is-windows": "^1.0.1" @@ -5694,11 +6644,10 @@ "node": ">=0.10.0" } }, - "node_modules/is-binary-path": { + "packages/blockly/node_modules/is-binary-path": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, + "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" }, @@ -5706,11 +6655,10 @@ "node": ">=8" } }, - "node_modules/is-core-module": { + "packages/blockly/node_modules/is-core-module": { "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", "dev": true, + "license": "MIT", "dependencies": { "hasown": "^2.0.0" }, @@ -5718,11 +6666,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-docker": { + "packages/blockly/node_modules/is-docker": { "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", "dev": true, + "license": "MIT", "bin": { "is-docker": "cli.js" }, @@ -5733,11 +6680,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-extendable": { + "packages/blockly/node_modules/is-extendable": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", "dev": true, + "license": "MIT", "dependencies": { "is-plain-object": "^2.0.4" }, @@ -5745,11 +6691,10 @@ "node": ">=0.10.0" } }, - "node_modules/is-extendable/node_modules/is-plain-object": { + "packages/blockly/node_modules/is-extendable/node_modules/is-plain-object": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "dev": true, + "license": "MIT", "dependencies": { "isobject": "^3.0.1" }, @@ -5757,29 +6702,18 @@ "node": ">=0.10.0" } }, - "node_modules/is-extglob": { + "packages/blockly/node_modules/is-extglob": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { + "packages/blockly/node_modules/is-glob": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -5787,49 +6721,43 @@ "node": ">=0.10.0" } }, - "node_modules/is-negated-glob": { + "packages/blockly/node_modules/is-negated-glob": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", - "integrity": "sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/is-number": { + "packages/blockly/node_modules/is-number": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } }, - "node_modules/is-plain-object": { + "packages/blockly/node_modules/is-plain-object": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/is-potential-custom-element-name": { + "packages/blockly/node_modules/is-potential-custom-element-name": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" + "license": "MIT" }, - "node_modules/is-promise": { + "packages/blockly/node_modules/is-promise": { "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", - "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/is-relative": { + "packages/blockly/node_modules/is-relative": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", - "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", "dev": true, + "license": "MIT", "dependencies": { "is-unc-path": "^1.0.0" }, @@ -5837,11 +6765,10 @@ "node": ">=0.10.0" } }, - "node_modules/is-stream": { + "packages/blockly/node_modules/is-stream": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -5849,11 +6776,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-unc-path": { + "packages/blockly/node_modules/is-unc-path": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", - "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", "dev": true, + "license": "MIT", "dependencies": { "unc-path-regex": "^0.1.2" }, @@ -5861,11 +6787,10 @@ "node": ">=0.10.0" } }, - "node_modules/is-unicode-supported": { + "packages/blockly/node_modules/is-unicode-supported": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -5873,29 +6798,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-valid-glob": { + "packages/blockly/node_modules/is-valid-glob": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", - "integrity": "sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/is-windows": { + "packages/blockly/node_modules/is-windows": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/is-wsl": { + "packages/blockly/node_modules/is-wsl": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "dev": true, + "license": "MIT", "dependencies": { "is-docker": "^2.0.0" }, @@ -5903,32 +6825,28 @@ "node": ">=8" } }, - "node_modules/isarray": { + "packages/blockly/node_modules/isarray": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/isexe": { + "packages/blockly/node_modules/isexe": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true + "dev": true, + "license": "ISC" }, - "node_modules/isobject": { + "packages/blockly/node_modules/isobject": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/istextorbinary": { + "packages/blockly/node_modules/istextorbinary": { "version": "3.3.0", - "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-3.3.0.tgz", - "integrity": "sha512-Tvq1W6NAcZeJ8op+Hq7tdZ434rqnMx4CCZ7H0ff83uEloDvVbqAwaMTZcafKGJT0VHkYzuXUiCY4hlXQg6WfoQ==", "dev": true, + "license": "MIT", "dependencies": { "binaryextensions": "^2.2.0", "textextensions": "^3.2.0" @@ -5940,10 +6858,8 @@ "url": "https://bevry.me/fund" } }, - "node_modules/jackspeak": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz", - "integrity": "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==", + "packages/blockly/node_modules/jackspeak": { + "version": "4.1.1", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -5956,63 +6872,46 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/jju": { + "packages/blockly/node_modules/jju": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", - "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", - "dev": true - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } + "license": "MIT" }, - "node_modules/jsbn": { + "packages/blockly/node_modules/jsbn": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/jsdoc-type-pratt-parser": { + "packages/blockly/node_modules/jsdoc-type-pratt-parser": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz", - "integrity": "sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==", "dev": true, + "license": "MIT", "engines": { "node": ">=12.0.0" } }, - "node_modules/jsdom": { - "version": "25.0.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", - "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "packages/blockly/node_modules/jsdom": { + "version": "26.1.0", + "license": "MIT", "dependencies": { - "cssstyle": "^4.1.0", + "cssstyle": "^4.2.1", "data-urls": "^5.0.0", - "decimal.js": "^10.4.3", - "form-data": "^4.0.0", + "decimal.js": "^10.5.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.5", + "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.12", - "parse5": "^7.1.2", - "rrweb-cssom": "^0.7.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^5.0.0", + "tough-cookie": "^5.1.1", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.0.0", + "whatwg-url": "^14.1.1", "ws": "^8.18.0", "xml-name-validator": "^5.0.0" }, @@ -6020,7 +6919,7 @@ "node": ">=18" }, "peerDependencies": { - "canvas": "^2.11.2" + "canvas": "^3.0.0" }, "peerDependenciesMeta": { "canvas": { @@ -6028,10 +6927,9 @@ } } }, - "node_modules/jsdom/node_modules/html-encoding-sniffer": { + "packages/blockly/node_modules/jsdom/node_modules/html-encoding-sniffer": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", - "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "license": "MIT", "dependencies": { "whatwg-encoding": "^3.1.1" }, @@ -6039,21 +6937,9 @@ "node": ">=18" } }, - "node_modules/jsdom/node_modules/tr46": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", - "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/jsdom/node_modules/whatwg-encoding": { + "packages/blockly/node_modules/jsdom/node_modules/whatwg-encoding": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "license": "MIT", "dependencies": { "iconv-lite": "0.6.3" }, @@ -6061,35 +6947,15 @@ "node": ">=18" } }, - "node_modules/jsdom/node_modules/whatwg-url": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", - "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", - "dependencies": { - "tr46": "^5.0.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/json-buffer": { + "packages/blockly/node_modules/json-buffer": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/json-stable-stringify": { + "packages/blockly/node_modules/json-stable-stringify": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.2.tgz", - "integrity": "sha512-eunSSaEnxV12z+Z73y/j5N37/In40GK4GmsSy+tEHJMxknvqnA7/djeYtAgW0GsWHUfg+847WJjKaEylk2y09g==", "dev": true, + "license": "MIT", "dependencies": { "jsonify": "^0.0.1" }, @@ -6097,26 +6963,23 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/json-stable-stringify-without-jsonify": { + "packages/blockly/node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/json-stringify-deterministic": { + "packages/blockly/node_modules/json-stringify-deterministic": { "version": "1.0.12", - "resolved": "https://registry.npmjs.org/json-stringify-deterministic/-/json-stringify-deterministic-1.0.12.tgz", - "integrity": "sha512-q3PN0lbUdv0pmurkBNdJH3pfFvOTL/Zp0lquqpvcjfKzt6Y0j49EPHAmVHCAS4Ceq/Y+PejWTzyiVpoY71+D6g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } }, - "node_modules/json5": { + "packages/blockly/node_modules/json5": { "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, + "license": "MIT", "bin": { "json5": "lib/cli.js" }, @@ -6124,11 +6987,10 @@ "node": ">=6" } }, - "node_modules/jsonfile": { + "packages/blockly/node_modules/jsonfile": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, + "license": "MIT", "dependencies": { "universalify": "^2.0.0" }, @@ -6136,20 +6998,18 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/jsonify": { + "packages/blockly/node_modules/jsonify": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", - "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", "dev": true, + "license": "Public Domain", "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/jszip": { + "packages/blockly/node_modules/jszip": { "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", "dev": true, + "license": "(MIT OR GPL-3.0-or-later)", "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", @@ -6157,11 +7017,10 @@ "setimmediate": "^1.0.5" } }, - "node_modules/jszip/node_modules/readable-stream": { + "packages/blockly/node_modules/jszip/node_modules/readable-stream": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, + "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -6172,50 +7031,44 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/just-curry-it": { + "packages/blockly/node_modules/just-curry-it": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/just-curry-it/-/just-curry-it-5.3.0.tgz", - "integrity": "sha512-silMIRiFjUWlfaDhkgSzpuAyQ6EX/o09Eu8ZBfmFwQMbax7+LQzeIU2CBrICT6Ne4l86ITCGvUCBpCubWYy0Yw==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/just-extend": { + "packages/blockly/node_modules/just-extend": { "version": "4.2.1", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", - "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/keyv": { + "packages/blockly/node_modules/keyv": { "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } }, - "node_modules/klaw-sync": { + "packages/blockly/node_modules/klaw-sync": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", - "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", "dev": true, + "license": "MIT", "dependencies": { "graceful-fs": "^4.1.11" } }, - "node_modules/last-run": { + "packages/blockly/node_modules/last-run": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/last-run/-/last-run-2.0.0.tgz", - "integrity": "sha512-j+y6WhTLN4Itnf9j5ZQos1BGPCS8DAwmgMroR3OzfxAsBxam0hMw7J8M3KqZl0pLQJ1jNnwIexg5DYpC/ctwEQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10.13.0" } }, - "node_modules/lazystream": { + "packages/blockly/node_modules/lazystream": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", - "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", "dev": true, + "license": "MIT", "dependencies": { "readable-stream": "^2.0.5" }, @@ -6223,11 +7076,10 @@ "node": ">= 0.6.3" } }, - "node_modules/lazystream/node_modules/readable-stream": { + "packages/blockly/node_modules/lazystream/node_modules/readable-stream": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, + "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -6238,20 +7090,18 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/lead": { + "packages/blockly/node_modules/lead": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/lead/-/lead-4.0.0.tgz", - "integrity": "sha512-DpMa59o5uGUWWjruMp71e6knmwKU3jRBBn1kjuLWN9EeIOxNeSAwvHf03WIl8g/ZMR2oSQC9ej3yeLBwdDc/pg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.13.0" } }, - "node_modules/levn": { + "packages/blockly/node_modules/levn": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -6260,20 +7110,18 @@ "node": ">= 0.8.0" } }, - "node_modules/lie": { + "packages/blockly/node_modules/lie": { "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", "dev": true, + "license": "MIT", "dependencies": { "immediate": "~3.0.5" } }, - "node_modules/liftoff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-5.0.0.tgz", - "integrity": "sha512-a5BQjbCHnB+cy+gsro8lXJ4kZluzOijzJ1UVVfyJYZC+IP2pLv1h4+aysQeKuTmyO8NAqfyQAk4HWaP/HjcKTg==", + "packages/blockly/node_modules/liftoff": { + "version": "5.0.1", "dev": true, + "license": "MIT", "dependencies": { "extend": "^3.0.2", "findup-sync": "^5.0.0", @@ -6287,10 +7135,8 @@ "node": ">=10.13.0" } }, - "node_modules/locate-app": { + "packages/blockly/node_modules/locate-app": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/locate-app/-/locate-app-2.5.0.tgz", - "integrity": "sha512-xIqbzPMBYArJRmPGUZD9CzV9wOqmVtQnaAn3wrj3s6WYW0bQvPI7x+sPYUGmDTYMHefVK//zc6HEYZ1qnxIK+Q==", "dev": true, "funding": [ { @@ -6302,17 +7148,17 @@ "url": "https://github.com/hejny/locate-app/blob/main/README.md#%EF%B8%8F-contributing" } ], + "license": "Apache-2.0", "dependencies": { "@promptbook/utils": "0.69.5", "type-fest": "4.26.0", "userhome": "1.0.1" } }, - "node_modules/locate-app/node_modules/type-fest": { + "packages/blockly/node_modules/locate-app/node_modules/type-fest": { "version": "4.26.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.0.tgz", - "integrity": "sha512-OduNjVJsFbifKb57UqZ2EMP1i4u64Xwow3NYXUtBbD4vIwJdQd4+xl8YDou1dlm4DVrtwT/7Ky8z8WyCULVfxw==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" }, @@ -6320,11 +7166,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/locate-path": { + "packages/blockly/node_modules/locate-path": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^5.0.0" }, @@ -6335,72 +7180,62 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { + "packages/blockly/node_modules/lodash": { "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/lodash._reinterpolate": { + "packages/blockly/node_modules/lodash._reinterpolate": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", - "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/lodash.assign": { + "packages/blockly/node_modules/lodash.assign": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", - "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/lodash.clonedeep": { + "packages/blockly/node_modules/lodash.clonedeep": { "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/lodash.get": { + "packages/blockly/node_modules/lodash.get": { "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true + "packages/blockly/node_modules/lodash.isequal": { + "version": "4.5.0", + "dev": true, + "license": "MIT" }, - "node_modules/lodash.template": { + "packages/blockly/node_modules/lodash.template": { "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", - "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", "dev": true, + "license": "MIT", "dependencies": { "lodash._reinterpolate": "^3.0.0", "lodash.templatesettings": "^4.0.0" } }, - "node_modules/lodash.templatesettings": { + "packages/blockly/node_modules/lodash.templatesettings": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", - "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", "dev": true, + "license": "MIT", "dependencies": { "lodash._reinterpolate": "^3.0.0" } }, - "node_modules/lodash.zip": { + "packages/blockly/node_modules/lodash.zip": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.zip/-/lodash.zip-4.2.0.tgz", - "integrity": "sha1-7GZi5IlkCO1KtsVCo5kLcswIACA=", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/log-symbols": { + "packages/blockly/node_modules/log-symbols": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" @@ -6412,11 +7247,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/loglevel": { + "packages/blockly/node_modules/loglevel": { "version": "1.9.2", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", - "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6.0" }, @@ -6425,26 +7259,15 @@ "url": "https://tidelift.com/funding/github/npm/loglevel" } }, - "node_modules/loglevel-plugin-prefix": { + "packages/blockly/node_modules/loglevel-plugin-prefix": { "version": "0.8.4", - "resolved": "https://registry.npmjs.org/loglevel-plugin-prefix/-/loglevel-plugin-prefix-0.8.4.tgz", - "integrity": "sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==", - "dev": true - }, - "node_modules/loupe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", - "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", "dev": true, - "dependencies": { - "get-func-name": "^2.0.1" - } + "license": "MIT" }, - "node_modules/lru-cache": { + "packages/blockly/node_modules/lru-cache": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, + "license": "ISC", "dependencies": { "yallist": "^4.0.0" }, @@ -6452,44 +7275,47 @@ "node": ">=10" } }, - "node_modules/lru-queue": { + "packages/blockly/node_modules/lru-queue": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", - "integrity": "sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM=", "dev": true, + "license": "MIT", "dependencies": { "es5-ext": "~0.10.2" } }, - "node_modules/map-cache": { + "packages/blockly/node_modules/map-cache": { "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/map-stream": { + "packages/blockly/node_modules/map-stream": { "version": "0.0.7", - "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz", - "integrity": "sha1-ih8HiW2CsQkmvTdEokIACfiJdKg=", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/markdown-tables-to-json": { + "packages/blockly/node_modules/markdown-tables-to-json": { "version": "0.1.7", - "resolved": "https://registry.npmjs.org/markdown-tables-to-json/-/markdown-tables-to-json-0.1.7.tgz", - "integrity": "sha512-1kdyYY9vKqmcsPHe7pRbrIeoapik1MOAEYtqlFoz0zypBf7yrtt0gP1UHOlk5kLuZQL1qaWgk0zYtOd7eJB0yA==", "dev": true, + "license": "MIT", "dependencies": { "@ts-stack/markdown": "^1.3.0" } }, - "node_modules/memoizee": { + "packages/blockly/node_modules/math-intrinsics": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "packages/blockly/node_modules/memoizee": { "version": "0.4.15", - "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz", - "integrity": "sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==", "dev": true, + "license": "ISC", "dependencies": { "d": "^1.0.1", "es5-ext": "^0.10.53", @@ -6501,26 +7327,21 @@ "timers-ext": "^0.1.7" } }, - "node_modules/memoizee/node_modules/next-tick": { + "packages/blockly/node_modules/memoizee/node_modules/next-tick": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", - "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", - "dev": true + "dev": true, + "license": "ISC" }, - "node_modules/merge2": { + "packages/blockly/node_modules/merge2": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, "license": "MIT", "engines": { "node": ">= 8" } }, - "node_modules/micromatch": { + "packages/blockly/node_modules/micromatch": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", "dependencies": { @@ -6531,11 +7352,10 @@ "node": ">=8.6" } }, - "node_modules/mime": { + "packages/blockly/node_modules/mime": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true, + "license": "MIT", "bin": { "mime": "cli.js" }, @@ -6543,30 +7363,10 @@ "node": ">=4" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimatch": { + "packages/blockly/node_modules/minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -6574,27 +7374,23 @@ "node": "*" } }, - "node_modules/minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", - "dev": true - }, - "node_modules/minipass": { + "packages/blockly/node_modules/minipass": { "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" } }, - "node_modules/mkdirp": { + "packages/blockly/node_modules/mitt": { + "version": "3.0.1", + "dev": true, + "license": "MIT" + }, + "packages/blockly/node_modules/mkdirp": { "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, + "license": "MIT", "dependencies": { "minimist": "^1.2.6" }, @@ -6602,31 +7398,30 @@ "mkdirp": "bin/cmd.js" } }, - "node_modules/mocha": { - "version": "10.7.3", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.7.3.tgz", - "integrity": "sha512-uQWxAu44wwiACGqjbPYmjo7Lg8sFrS3dQe7PP2FQI+woptP4vZXSMcfMyFL/e1yFEeEpV4RtyTpZROOKmxis+A==", + "packages/blockly/node_modules/mocha": { + "version": "11.7.2", "dev": true, + "license": "MIT", "dependencies": { - "ansi-colors": "^4.1.3", "browser-stdout": "^1.3.1", - "chokidar": "^3.5.3", + "chokidar": "^4.0.1", "debug": "^4.3.5", - "diff": "^5.2.0", + "diff": "^7.0.0", "escape-string-regexp": "^4.0.0", "find-up": "^5.0.0", - "glob": "^8.1.0", + "glob": "^10.4.5", "he": "^1.2.0", "js-yaml": "^4.1.0", "log-symbols": "^4.1.0", - "minimatch": "^5.1.6", + "minimatch": "^9.0.5", "ms": "^2.1.3", + "picocolors": "^1.1.1", "serialize-javascript": "^6.0.2", "strip-json-comments": "^3.1.1", "supports-color": "^8.1.1", - "workerpool": "^6.5.1", - "yargs": "^16.2.0", - "yargs-parser": "^20.2.9", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", "yargs-unparser": "^2.0.0" }, "bin": { @@ -6634,55 +7429,119 @@ "mocha": "bin/mocha.js" }, "engines": { - "node": ">= 14.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/mocha/node_modules/brace-expansion": { + "packages/blockly/node_modules/mocha/node_modules/brace-expansion": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, - "node_modules/mocha/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "packages/blockly/node_modules/mocha/node_modules/chokidar": { + "version": "4.0.3", "dev": true, + "license": "MIT", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" + "readdirp": "^4.0.1" }, "engines": { - "node": ">=12" + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "packages/blockly/node_modules/mocha/node_modules/glob": { + "version": "10.4.5", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/mocha/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "packages/blockly/node_modules/mocha/node_modules/jackspeak": { + "version": "3.4.3", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "packages/blockly/node_modules/mocha/node_modules/lru-cache": { + "version": "10.4.3", "dev": true, + "license": "ISC" + }, + "packages/blockly/node_modules/mocha/node_modules/minimatch": { + "version": "9.0.5", + "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=10" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "packages/blockly/node_modules/mocha/node_modules/path-scurry": { + "version": "1.11.1", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "packages/blockly/node_modules/mocha/node_modules/picocolors": { + "version": "1.1.1", + "dev": true, + "license": "ISC" + }, + "packages/blockly/node_modules/mocha/node_modules/readdirp": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, - "node_modules/mocha/node_modules/supports-color": { + "packages/blockly/node_modules/mocha/node_modules/supports-color": { "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -6693,79 +7552,45 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/mocha/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mocha/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/monaco-editor": { + "packages/blockly/node_modules/monaco-editor": { "version": "0.20.0", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.20.0.tgz", - "integrity": "sha512-hkvf4EtPJRMQlPC3UbMoRs0vTAFAYdzFQ+gpMb8A+9znae1c43q8Mab9iVsgTcg/4PNiLGGn3SlDIa8uvK1FIQ==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/ms": { + "packages/blockly/node_modules/ms": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "license": "MIT" }, - "node_modules/mute-stdout": { + "packages/blockly/node_modules/mute-stdout": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-2.0.0.tgz", - "integrity": "sha512-32GSKM3Wyc8dg/p39lWPKYu8zci9mJFzV1Np9Of0ZEpe6Fhssn/FbI7ywAMd40uX+p3ZKh3T5EeCFv81qS3HmQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10.13.0" } }, - "node_modules/natural-compare": { + "packages/blockly/node_modules/natural-compare": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/netmask": { + "packages/blockly/node_modules/netmask": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", - "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4.0" } }, - "node_modules/next-tick": { + "packages/blockly/node_modules/next-tick": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", - "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/nise": { + "packages/blockly/node_modules/nise": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/nise/-/nise-4.1.0.tgz", - "integrity": "sha512-eQMEmGN/8arp0xsvGoQ+B1qvSkR73B1nWSCh7nOt5neMCtwcQVYQGdzQMhcNscktTsWB54xnlSQFzOAPJD8nXA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^1.7.0", "@sinonjs/fake-timers": "^6.0.0", @@ -6774,10 +7599,8 @@ "path-to-regexp": "^1.7.0" } }, - "node_modules/node-domexception": { + "packages/blockly/node_modules/node-domexception": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", "dev": true, "funding": [ { @@ -6789,15 +7612,15 @@ "url": "https://paypal.me/jimmywarting" } ], + "license": "MIT", "engines": { "node": ">=10.5.0" } }, - "node_modules/node-fetch": { + "packages/blockly/node_modules/node-fetch": { "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "dev": true, + "license": "MIT", "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", @@ -6811,20 +7634,18 @@ "url": "https://opencollective.com/node-fetch" } }, - "node_modules/normalize-path": { + "packages/blockly/node_modules/normalize-path": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/now-and-later": { + "packages/blockly/node_modules/now-and-later": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-3.0.0.tgz", - "integrity": "sha512-pGO4pzSdaxhWTGkfSfHx3hVzJVslFPwBp2Myq9MYN/ChfJZF87ochMAXnvz6/58RJSf5ik2q9tXprBBrk2cpcg==", "dev": true, + "license": "MIT", "dependencies": { "once": "^1.4.0" }, @@ -6832,11 +7653,10 @@ "node": ">= 10.13.0" } }, - "node_modules/nth-check": { + "packages/blockly/node_modules/nth-check": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0" }, @@ -6844,25 +7664,33 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, - "node_modules/nwsapi": { - "version": "2.2.12", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.12.tgz", - "integrity": "sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==" + "packages/blockly/node_modules/nwsapi": { + "version": "2.2.20", + "license": "MIT" }, - "node_modules/object-assign": { + "packages/blockly/node_modules/object-assign": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/object.defaults": { + "packages/blockly/node_modules/object-inspect": { + "version": "1.13.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "packages/blockly/node_modules/object.defaults": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", - "integrity": "sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==", "dev": true, + "license": "MIT", "dependencies": { "array-each": "^1.0.1", "array-slice": "^1.0.0", @@ -6873,11 +7701,10 @@ "node": ">=0.10.0" } }, - "node_modules/object.pick": { + "packages/blockly/node_modules/object.pick": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", "dev": true, + "license": "MIT", "dependencies": { "isobject": "^3.0.1" }, @@ -6885,20 +7712,18 @@ "node": ">=0.10.0" } }, - "node_modules/once": { + "packages/blockly/node_modules/once": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, + "license": "ISC", "dependencies": { "wrappy": "1" } }, - "node_modules/open": { + "packages/blockly/node_modules/open": { "version": "7.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", - "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", "dev": true, + "license": "MIT", "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" @@ -6910,20 +7735,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/opener": { + "packages/blockly/node_modules/opener": { "version": "1.5.2", - "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", - "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", "dev": true, + "license": "(WTFPL OR MIT)", "bin": { "opener": "bin/opener-bin.js" } }, - "node_modules/optionator": { + "packages/blockly/node_modules/optionator": { "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, + "license": "MIT", "dependencies": { "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", @@ -6936,20 +7759,10 @@ "node": ">= 0.8.0" } }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/p-limit": { + "packages/blockly/node_modules/p-limit": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, + "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, @@ -6960,11 +7773,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-locate": { + "packages/blockly/node_modules/p-locate": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^3.0.2" }, @@ -6975,11 +7787,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/pac-proxy-agent": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.1.0.tgz", - "integrity": "sha512-Z5FnLVVZSnX7WjBg0mhDtydeRZ1xMcATZThjySQUHqr+0ksP8kqaw23fNKkaaN/Z8gwLUs/W7xdl0I75eP2Xyw==", + "packages/blockly/node_modules/pac-proxy-agent": { + "version": "7.2.0", "dev": true, + "license": "MIT", "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", @@ -6994,11 +7805,10 @@ "node": ">= 14" } }, - "node_modules/pac-resolver": { + "packages/blockly/node_modules/pac-resolver": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", "dev": true, + "license": "MIT", "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" @@ -7007,36 +7817,20 @@ "node": ">= 14" } }, - "node_modules/package-json-from-dist": { + "packages/blockly/node_modules/package-json-from-dist": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true, "license": "BlueOak-1.0.0" }, - "node_modules/pako": { + "packages/blockly/node_modules/pako": { "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } + "license": "(MIT AND Zlib)" }, - "node_modules/parse-filepath": { + "packages/blockly/node_modules/parse-filepath": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", - "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", "dev": true, + "license": "MIT", "dependencies": { "is-absolute": "^1.0.0", "map-cache": "^0.2.0", @@ -7046,53 +7840,49 @@ "node": ">=0.8" } }, - "node_modules/parse-imports": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/parse-imports/-/parse-imports-2.2.1.tgz", - "integrity": "sha512-OL/zLggRp8mFhKL0rNORUTR4yBYujK/uU+xZL+/0Rgm2QE4nLO9v8PzEweSJEbMGKmDRjJE4R3IMJlL2di4JeQ==", + "packages/blockly/node_modules/parse-imports-exports": { + "version": "0.2.4", "dev": true, + "license": "MIT", "dependencies": { - "es-module-lexer": "^1.5.3", - "slashes": "^3.0.12" - }, - "engines": { - "node": ">= 18" + "parse-statements": "1.0.11" } }, - "node_modules/parse-node-version": { + "packages/blockly/node_modules/parse-node-version": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", - "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.10" } }, - "node_modules/parse-passwd": { + "packages/blockly/node_modules/parse-passwd": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "packages/blockly/node_modules/parse-statements": { + "version": "1.0.11", + "dev": true, + "license": "MIT" + }, + "packages/blockly/node_modules/parse5": { + "version": "7.2.1", + "license": "MIT", "dependencies": { - "entities": "^4.4.0" + "entities": "^4.5.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/parse5-htmlparser2-tree-adapter": { + "packages/blockly/node_modules/parse5-htmlparser2-tree-adapter": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", - "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", "dev": true, + "license": "MIT", "dependencies": { "domhandler": "^5.0.2", "parse5": "^7.0.0" @@ -7101,11 +7891,10 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/parse5-parser-stream": { + "packages/blockly/node_modules/parse5-parser-stream": { "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", - "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", "dev": true, + "license": "MIT", "dependencies": { "parse5": "^7.0.0" }, @@ -7113,26 +7902,24 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/patch-package": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.0.tgz", - "integrity": "sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==", + "packages/blockly/node_modules/patch-package": { + "version": "8.0.1", "dev": true, + "license": "MIT", "dependencies": { "@yarnpkg/lockfile": "^1.1.0", "chalk": "^4.1.2", "ci-info": "^3.7.0", "cross-spawn": "^7.0.3", "find-yarn-workspace-root": "^2.0.0", - "fs-extra": "^9.0.0", + "fs-extra": "^10.0.0", "json-stable-stringify": "^1.0.2", "klaw-sync": "^6.0.0", "minimist": "^1.2.6", "open": "^7.4.2", - "rimraf": "^2.6.3", "semver": "^7.5.3", "slash": "^2.0.0", - "tmp": "^0.0.33", + "tmp": "^0.2.4", "yaml": "^2.2.2" }, "bin": { @@ -7143,100 +7930,52 @@ "npm": ">5" } }, - "node_modules/patch-package/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "packages/blockly/node_modules/patch-package/node_modules/fs-extra": { + "version": "10.1.0", "dev": true, + "license": "MIT", "dependencies": { - "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" }, "engines": { - "node": ">=10" - } - }, - "node_modules/patch-package/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/patch-package/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" + "node": ">=12" } }, - "node_modules/patch-package/node_modules/slash": { + "packages/blockly/node_modules/patch-package/node_modules/slash": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/path-exists": { + "packages/blockly/node_modules/path-exists": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { + "packages/blockly/node_modules/path-key": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/path-parse": { + "packages/blockly/node_modules/path-parse": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/path-root": { + "packages/blockly/node_modules/path-root": { "version": "0.1.1", - "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", - "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", "dev": true, + "license": "MIT", "dependencies": { "path-root-regex": "^0.1.0" }, @@ -7244,19 +7983,16 @@ "node": ">=0.10.0" } }, - "node_modules/path-root-regex": { + "packages/blockly/node_modules/path-root-regex": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", - "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/path-scurry": { + "packages/blockly/node_modules/path-scurry": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -7270,57 +8006,41 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/path-scurry/node_modules/lru-cache": { + "packages/blockly/node_modules/path-scurry/node_modules/lru-cache": { "version": "11.0.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", - "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", "dev": true, "license": "ISC", "engines": { "node": "20 || >=22" } }, - "node_modules/path-to-regexp": { + "packages/blockly/node_modules/path-to-regexp": { "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", "dev": true, + "license": "MIT", "dependencies": { "isarray": "0.0.1" } }, - "node_modules/path-to-regexp/node_modules/isarray": { + "packages/blockly/node_modules/path-to-regexp/node_modules/isarray": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "node_modules/pathval": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", - "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", "dev": true, - "engines": { - "node": ">= 14.16" - } + "license": "MIT" }, - "node_modules/pend": { + "packages/blockly/node_modules/pend": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/picocolors": { + "packages/blockly/node_modules/picocolors": { "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true + "dev": true, + "license": "ISC" }, - "node_modules/picomatch": { + "packages/blockly/node_modules/picomatch": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -7328,11 +8048,10 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/plugin-error": { + "packages/blockly/node_modules/plugin-error": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", - "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-colors": "^1.0.1", "arr-diff": "^4.0.0", @@ -7343,11 +8062,10 @@ "node": ">= 0.10" } }, - "node_modules/plugin-error/node_modules/ansi-colors": { + "packages/blockly/node_modules/plugin-error/node_modules/ansi-colors": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", - "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-wrap": "^0.1.0" }, @@ -7355,11 +8073,10 @@ "node": ">=0.10.0" } }, - "node_modules/portfinder": { + "packages/blockly/node_modules/portfinder": { "version": "1.0.28", - "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", - "integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==", "dev": true, + "license": "MIT", "dependencies": { "async": "^2.6.2", "debug": "^3.1.1", @@ -7369,29 +8086,26 @@ "node": ">= 0.12.0" } }, - "node_modules/portfinder/node_modules/async": { + "packages/blockly/node_modules/portfinder/node_modules/async": { "version": "2.6.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", "dev": true, + "license": "MIT", "dependencies": { "lodash": "^4.17.14" } }, - "node_modules/portfinder/node_modules/debug": { + "packages/blockly/node_modules/portfinder/node_modules/debug": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.1" } }, - "node_modules/postcss": { + "packages/blockly/node_modules/postcss": { "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", "dev": true, + "license": "MIT", "dependencies": { "picocolors": "^0.2.1", "source-map": "^0.6.1" @@ -7404,28 +8118,24 @@ "url": "https://opencollective.com/postcss/" } }, - "node_modules/postcss/node_modules/source-map": { + "packages/blockly/node_modules/postcss/node_modules/source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, - "node_modules/prelude-ls": { + "packages/blockly/node_modules/prelude-ls": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8.0" } }, - "node_modules/prettier": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", - "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "packages/blockly/node_modules/prettier": { + "version": "3.6.2", "dev": true, "license": "MIT", "bin": { @@ -7438,11 +8148,10 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/prettier-linter-helpers": { + "packages/blockly/node_modules/prettier-linter-helpers": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", "dev": true, + "license": "MIT", "dependencies": { "fast-diff": "^1.1.2" }, @@ -7450,15 +8159,14 @@ "node": ">=6.0.0" } }, - "node_modules/prettier-plugin-organize-imports": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.1.0.tgz", - "integrity": "sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A==", + "packages/blockly/node_modules/prettier-plugin-organize-imports": { + "version": "4.3.0", "dev": true, + "license": "MIT", "peerDependencies": { "prettier": ">=2.0", "typescript": ">=2.9", - "vue-tsc": "^2.1.0" + "vue-tsc": "^2.1.0 || 3" }, "peerDependenciesMeta": { "vue-tsc": { @@ -7466,35 +8174,31 @@ } } }, - "node_modules/process": { + "packages/blockly/node_modules/process": { "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6.0" } }, - "node_modules/process-nextick-args": { + "packages/blockly/node_modules/process-nextick-args": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/progress": { + "packages/blockly/node_modules/progress": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.4.0" } }, - "node_modules/proxy-agent": { + "packages/blockly/node_modules/proxy-agent": { "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", "dev": true, + "license": "MIT", "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", @@ -7509,58 +8213,73 @@ "node": ">= 14" } }, - "node_modules/proxy-agent/node_modules/lru-cache": { + "packages/blockly/node_modules/proxy-agent/node_modules/lru-cache": { "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", "dev": true, + "license": "ISC", "engines": { "node": ">=12" } }, - "node_modules/proxy-from-env": { + "packages/blockly/node_modules/proxy-from-env": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/pump": { + "packages/blockly/node_modules/pump": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", "dev": true, + "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, - "node_modules/punycode": { + "packages/blockly/node_modules/punycode": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "packages/blockly/node_modules/puppeteer-core": { + "version": "24.20.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.10.9", + "chromium-bidi": "8.0.0", + "debug": "^4.4.1", + "devtools-protocol": "0.0.1495869", + "typed-query-selector": "^2.12.0", + "webdriver-bidi-protocol": "0.2.8", + "ws": "^8.18.3" + }, + "engines": { + "node": ">=18" + } + }, + "packages/blockly/node_modules/qs": { + "version": "6.14.0", "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, "engines": { "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/query-selector-shadow-dom": { + "packages/blockly/node_modules/query-selector-shadow-dom": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz", - "integrity": "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/queue-microtask": { + "packages/blockly/node_modules/queue-microtask": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true, "funding": [ { @@ -7578,26 +8297,18 @@ ], "license": "MIT" }, - "node_modules/queue-tick": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", - "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", - "dev": true - }, - "node_modules/randombytes": { + "packages/blockly/node_modules/randombytes": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "^5.1.0" } }, - "node_modules/readable-stream": { + "packages/blockly/node_modules/readable-stream": { "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, + "license": "MIT", "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -7607,29 +8318,26 @@ "node": ">= 6" } }, - "node_modules/readdir-glob": { + "packages/blockly/node_modules/readdir-glob": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", - "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "minimatch": "^5.1.0" } }, - "node_modules/readdir-glob/node_modules/brace-expansion": { + "packages/blockly/node_modules/readdir-glob/node_modules/brace-expansion": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, - "node_modules/readdir-glob/node_modules/minimatch": { + "packages/blockly/node_modules/readdir-glob/node_modules/minimatch": { "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -7637,11 +8345,10 @@ "node": ">=10" } }, - "node_modules/readdirp": { + "packages/blockly/node_modules/readdirp": { "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, + "license": "MIT", "dependencies": { "picomatch": "^2.2.1" }, @@ -7649,20 +8356,18 @@ "node": ">=8.10.0" } }, - "node_modules/readline-sync": { + "packages/blockly/node_modules/readline-sync": { "version": "1.4.10", - "resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz", - "integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8.0" } }, - "node_modules/rechoir": { + "packages/blockly/node_modules/rechoir": { "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", - "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", "dev": true, + "license": "MIT", "dependencies": { "resolve": "^1.20.0" }, @@ -7670,55 +8375,36 @@ "node": ">= 10.13.0" } }, - "node_modules/remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true - }, - "node_modules/replace-ext": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", - "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/replace-homedir": { + "packages/blockly/node_modules/replace-homedir": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/replace-homedir/-/replace-homedir-2.0.0.tgz", - "integrity": "sha512-bgEuQQ/BHW0XkkJtawzrfzHFSN70f/3cNOiHa2QsYxqrjaC30X1k74FJ6xswVBP0sr0SpGIdVFuPwfrYziVeyw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10.13.0" } }, - "node_modules/replacestream": { + "packages/blockly/node_modules/replacestream": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/replacestream/-/replacestream-4.0.3.tgz", - "integrity": "sha512-AC0FiLS352pBBiZhd4VXB1Ab/lh0lEgpP+GGvZqbQh8a5cmXVoTe5EX/YeTFArnp4SRGTHh1qCHu9lGs1qG8sA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "escape-string-regexp": "^1.0.3", "object-assign": "^4.0.1", "readable-stream": "^2.0.2" } }, - "node_modules/replacestream/node_modules/escape-string-regexp": { + "packages/blockly/node_modules/replacestream/node_modules/escape-string-regexp": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.0" } }, - "node_modules/replacestream/node_modules/readable-stream": { + "packages/blockly/node_modules/replacestream/node_modules/readable-stream": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, + "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -7729,35 +8415,15 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/requires-port": { + "packages/blockly/node_modules/requires-port": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/resolve": { + "packages/blockly/node_modules/resolve": { "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dev": true, + "license": "MIT", "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -7770,33 +8436,22 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve-dir": { + "packages/blockly/node_modules/resolve-dir": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", - "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", "dev": true, + "license": "MIT", "dependencies": { - "expand-tilde": "^2.0.0", - "global-modules": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, "engines": { - "node": ">=4" + "node": ">=0.10.0" } }, - "node_modules/resolve-options": { + "packages/blockly/node_modules/resolve-options": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-2.0.0.tgz", - "integrity": "sha512-/FopbmmFOQCfsCx77BRFdKOniglTiHumLgwvd6IDPihy1GKkadZbgQJBcTb2lMzSR1pndzd96b1nZrreZ7+9/A==", "dev": true, + "license": "MIT", "dependencies": { "value-or-function": "^4.0.0" }, @@ -7804,42 +8459,37 @@ "node": ">= 10.13.0" } }, - "node_modules/resq": { + "packages/blockly/node_modules/resq": { "version": "1.11.0", - "resolved": "https://registry.npmjs.org/resq/-/resq-1.11.0.tgz", - "integrity": "sha512-G10EBz+zAAy3zUd/CDoBbXRL6ia9kOo3xRHrMDsHljI0GDkhYlyjwoCx5+3eCC4swi1uCoZQhskuJkj7Gp57Bw==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^2.0.1" } }, - "node_modules/resq/node_modules/fast-deep-equal": { + "packages/blockly/node_modules/resq/node_modules/fast-deep-equal": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/reusify": { + "packages/blockly/node_modules/reusify": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true, + "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" } }, - "node_modules/rgb2hex": { + "packages/blockly/node_modules/rgb2hex": { "version": "0.2.5", - "resolved": "https://registry.npmjs.org/rgb2hex/-/rgb2hex-0.2.5.tgz", - "integrity": "sha512-22MOP1Rh7sAo1BZpDG6R5RFYzR2lYEgwq7HEmyW2qcsOqR2lQKmn+O//xV3YG/0rrhMC6KVX2hU+ZXuaw9a5bw==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/rimraf": { + "packages/blockly/node_modules/rimraf": { "version": "5.0.10", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", - "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", "dev": true, + "license": "ISC", "dependencies": { "glob": "^10.3.7" }, @@ -7850,20 +8500,16 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rimraf/node_modules/brace-expansion": { + "packages/blockly/node_modules/rimraf/node_modules/brace-expansion": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, - "node_modules/rimraf/node_modules/glob": { + "packages/blockly/node_modules/rimraf/node_modules/glob": { "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, "license": "ISC", "dependencies": { @@ -7881,10 +8527,8 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rimraf/node_modules/jackspeak": { + "packages/blockly/node_modules/rimraf/node_modules/jackspeak": { "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -7897,17 +8541,13 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/rimraf/node_modules/lru-cache": { + "packages/blockly/node_modules/rimraf/node_modules/lru-cache": { "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, "license": "ISC" }, - "node_modules/rimraf/node_modules/minimatch": { + "packages/blockly/node_modules/rimraf/node_modules/minimatch": { "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "license": "ISC", "dependencies": { @@ -7920,10 +8560,8 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rimraf/node_modules/path-scurry": { + "packages/blockly/node_modules/rimraf/node_modules/path-scurry": { "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -7937,15 +8575,12 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rrweb-cssom": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", - "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==" + "packages/blockly/node_modules/rrweb-cssom": { + "version": "0.8.0", + "license": "MIT" }, - "node_modules/run-parallel": { + "packages/blockly/node_modules/run-parallel": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, "funding": [ { @@ -7966,45 +8601,39 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "packages/blockly/node_modules/rxjs": { + "version": "7.8.2", "dev": true, + "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" } }, - "node_modules/rxjs/node_modules/tslib": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz", - "integrity": "sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==", - "dev": true + "packages/blockly/node_modules/rxjs/node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "license": "0BSD" }, - "node_modules/safaridriver": { + "packages/blockly/node_modules/safaridriver": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safaridriver/-/safaridriver-1.0.0.tgz", - "integrity": "sha512-J92IFbskyo7OYB3Dt4aTdyhag1GlInrfbPCmMteb7aBK7PwlnGz1HI0+oyNN97j7pV9DqUAVoVgkNRMrfY47mQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18.0.0" } }, - "node_modules/safe-buffer": { + "packages/blockly/node_modules/safe-buffer": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/safer-buffer": { + "packages/blockly/node_modules/safer-buffer": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "license": "MIT" }, - "node_modules/saxes": { + "packages/blockly/node_modules/saxes": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "license": "ISC", "dependencies": { "xmlchars": "^2.2.0" }, @@ -8012,17 +8641,15 @@ "node": ">=v12.22.7" } }, - "node_modules/secure-compare": { + "packages/blockly/node_modules/secure-compare": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", - "integrity": "sha1-8aAymzCLIh+uN7mXTz1XjQypmeM=", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/semver": { + "packages/blockly/node_modules/semver": { "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, + "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -8033,11 +8660,10 @@ "node": ">=10" } }, - "node_modules/semver-greatest-satisfied-range": { + "packages/blockly/node_modules/semver-greatest-satisfied-range": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-2.0.0.tgz", - "integrity": "sha512-lH3f6kMbwyANB7HuOWRMlLCa2itaCrZJ+SAqqkSZrZKO/cAsk2EOyaKHUtNkVLFyFW9pct22SFesFp3Z7zpA0g==", "dev": true, + "license": "MIT", "dependencies": { "sver": "^1.8.3" }, @@ -8045,11 +8671,10 @@ "node": ">= 10.13.0" } }, - "node_modules/serialize-error": { + "packages/blockly/node_modules/serialize-error": { "version": "11.0.3", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-11.0.3.tgz", - "integrity": "sha512-2G2y++21dhj2R7iHAdd0FIzjGwuKZld+7Pl/bTU6YIkrC2ZMbVUjm+luj6A6V34Rv9XfKJDKpTWu9W4Gse1D9g==", "dev": true, + "license": "MIT", "dependencies": { "type-fest": "^2.12.2" }, @@ -8060,26 +8685,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/serialize-javascript": { + "packages/blockly/node_modules/serialize-javascript": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "randombytes": "^2.1.0" } }, - "node_modules/setimmediate": { + "packages/blockly/node_modules/setimmediate": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/shebang-command": { + "packages/blockly/node_modules/shebang-command": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -8087,29 +8709,97 @@ "node": ">=8" } }, - "node_modules/shebang-regex": { + "packages/blockly/node_modules/shebang-regex": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/shell-quote": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", - "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "packages/blockly/node_modules/shell-quote": { + "version": "1.8.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "packages/blockly/node_modules/side-channel": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "packages/blockly/node_modules/side-channel-list": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "packages/blockly/node_modules/side-channel-map": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "packages/blockly/node_modules/side-channel-weakmap": { + "version": "1.0.2", "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/signal-exit": { + "packages/blockly/node_modules/signal-exit": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.1.tgz", - "integrity": "sha512-uUWsN4aOxJAS8KOuf3QMyFtgm1pkb6I+KRZbRF/ghdf5T7sM+B1lLLzPDxswUjkmHyxQAVzEgG35E3NzDM9GVw==", "dev": true, + "license": "ISC", "engines": { "node": ">=14" }, @@ -8117,11 +8807,10 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/sinon": { + "packages/blockly/node_modules/sinon": { "version": "9.2.4", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.4.tgz", - "integrity": "sha512-zljcULZQsJxVra28qIAL6ow1Z9tpattkCTEJR4RBP3TGc00FcttsP5pK284Nas5WjMZU5Yzy3kAIp3B3KRf5Yg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^1.8.1", "@sinonjs/fake-timers": "^6.0.1", @@ -8135,36 +8824,27 @@ "url": "https://opencollective.com/sinon" } }, - "node_modules/sinon/node_modules/diff": { + "packages/blockly/node_modules/sinon/node_modules/diff": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } }, - "node_modules/slashes": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/slashes/-/slashes-3.0.12.tgz", - "integrity": "sha512-Q9VME8WyGkc7pJf6QEkj3wE+2CnvZMI+XJhwdTPR8Z/kWQRXi7boAWLDibRPyHRTUTPx5FaU7MsyrjI3yLB4HA==", - "dev": true - }, - "node_modules/smart-buffer": { + "packages/blockly/node_modules/smart-buffer": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" } }, - "node_modules/socks": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", - "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "packages/blockly/node_modules/socks": { + "version": "2.8.4", "dev": true, + "license": "MIT", "dependencies": { "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" @@ -8174,11 +8854,10 @@ "npm": ">= 3.0.0" } }, - "node_modules/socks-proxy-agent": { + "packages/blockly/node_modules/socks-proxy-agent": { "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", "dev": true, + "license": "MIT", "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", @@ -8188,29 +8867,17 @@ "node": ">= 14" } }, - "node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-resolve": { + "packages/blockly/node_modules/source-map-resolve": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", - "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==", "dev": true, + "license": "MIT", "dependencies": { "atob": "^2.1.2", "decode-uri-component": "^0.2.0" } }, - "node_modules/spacetrim": { + "packages/blockly/node_modules/spacetrim": { "version": "0.11.59", - "resolved": "https://registry.npmjs.org/spacetrim/-/spacetrim-0.11.59.tgz", - "integrity": "sha512-lLYsktklSRKprreOm7NXReW8YiX2VBjbgmXYEziOoGf/qsJqAEACaDvoTtUOycwjpaSh+bT8eu0KrJn7UNxiCg==", "dev": true, "funding": [ { @@ -8221,72 +8888,55 @@ "type": "github", "url": "https://github.com/hejny/spacetrim/blob/main/README.md#%EF%B8%8F-contributing" } - ] + ], + "license": "Apache-2.0" }, - "node_modules/sparkles": { + "packages/blockly/node_modules/sparkles": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-2.1.0.tgz", - "integrity": "sha512-r7iW1bDw8R/cFifrD3JnQJX0K1jqT0kprL48BiBpLZLJPmAm34zsVBsK5lc7HirZYZqMW65dOXZgbAGt/I6frg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10.13.0" } }, - "node_modules/spdx-exceptions": { + "packages/blockly/node_modules/spdx-exceptions": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", - "dev": true + "dev": true, + "license": "CC-BY-3.0" }, - "node_modules/spdx-license-ids": { + "packages/blockly/node_modules/spdx-license-ids": { "version": "3.0.11", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz", - "integrity": "sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==", - "dev": true - }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", "dev": true, - "engines": { - "node": ">= 10.x" - } + "license": "CC0-1.0" }, - "node_modules/sprintf-js": { + "packages/blockly/node_modules/sprintf-js": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, - "node_modules/stream-composer": { + "packages/blockly/node_modules/stream-composer": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz", - "integrity": "sha512-bnBselmwfX5K10AH6L4c8+S5lgZMWI7ZYrz2rvYjCPB2DIMC4Ig8OpxGpNJSxRZ58oti7y1IcNvjBAz9vW5m4w==", "dev": true, + "license": "MIT", "dependencies": { "streamx": "^2.13.2" } }, - "node_modules/stream-exhaust": { + "packages/blockly/node_modules/stream-exhaust": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/stream-exhaust/-/stream-exhaust-1.0.2.tgz", - "integrity": "sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/stream-to-array": { + "packages/blockly/node_modules/stream-to-array": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/stream-to-array/-/stream-to-array-2.3.0.tgz", - "integrity": "sha512-UsZtOYEn4tWU2RGLOXr/o/xjRBftZRlG3dEWoaHr8j4GuypJ3isitGbVyjQKAuMu+xbiop8q224TjiZWc4XTZA==", "dev": true, + "license": "MIT", "dependencies": { "any-promise": "^1.1.0" } }, - "node_modules/streamqueue": { + "packages/blockly/node_modules/streamqueue": { "version": "0.0.6", - "resolved": "https://registry.npmjs.org/streamqueue/-/streamqueue-0.0.6.tgz", - "integrity": "sha1-ZvX17JTpuK8knkrsLdH3Qb/pTeM=", "dev": true, "dependencies": { "readable-stream": "^1.0.26-2" @@ -8295,17 +8945,15 @@ "node": ">= 0.10.0" } }, - "node_modules/streamqueue/node_modules/isarray": { + "packages/blockly/node_modules/streamqueue/node_modules/isarray": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/streamqueue/node_modules/readable-stream": { + "packages/blockly/node_modules/streamqueue/node_modules/readable-stream": { "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", "dev": true, + "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", @@ -8313,63 +8961,30 @@ "string_decoder": "~0.10.x" } }, - "node_modules/streamqueue/node_modules/string_decoder": { + "packages/blockly/node_modules/streamqueue/node_modules/string_decoder": { "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - }, - "node_modules/streamx": { - "version": "2.21.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.21.1.tgz", - "integrity": "sha512-PhP9wUnFLa+91CPy3N6tiQsK+gnYyUNuk15S3YG/zjYE7RuPeCjJngqnzpC31ow0lzBHQ+QGO4cNJnd0djYUsw==", "dev": true, - "dependencies": { - "fast-fifo": "^1.3.2", - "queue-tick": "^1.0.1", - "text-decoder": "^1.1.0" - }, - "optionalDependencies": { - "bare-events": "^2.2.0" - } + "license": "MIT" }, - "node_modules/string_decoder": { + "packages/blockly/node_modules/string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" } }, - "node_modules/string-argv": { + "packages/blockly/node_modules/string-argv": { "version": "0.3.1", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", - "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.6.19" } }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs": { + "packages/blockly/node_modules/string-width-cjs": { "name": "string-width", "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { @@ -8381,23 +8996,9 @@ "node": ">=8" } }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { + "packages/blockly/node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -8407,39 +9008,26 @@ "node": ">=8" } }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "packages/blockly/node_modules/strip-ansi-cjs/node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom-string": { + "packages/blockly/node_modules/strip-bom-string": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", - "integrity": "sha1-5SEekiQ2n7uB1jOi8ABE3IztrZI=", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/strip-json-comments": { + "packages/blockly/node_modules/strip-json-comments": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -8447,17 +9035,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strnum": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", - "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", - "dev": true + "packages/blockly/node_modules/strnum": { + "version": "1.1.2", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" }, - "node_modules/supports-color": { + "packages/blockly/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -8465,11 +9057,10 @@ "node": ">=8" } }, - "node_modules/supports-preserve-symlinks-flag": { + "packages/blockly/node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -8477,100 +9068,68 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/sver": { + "packages/blockly/node_modules/sver": { "version": "1.8.4", - "resolved": "https://registry.npmjs.org/sver/-/sver-1.8.4.tgz", - "integrity": "sha512-71o1zfzyawLfIWBOmw8brleKyvnbn73oVHNCsu51uPMz/HWiKkkXsI31JjHW5zqXEqnPYkIiHd8ZmL7FCimLEA==", "dev": true, + "license": "MIT", "optionalDependencies": { "semver": "^6.3.0" } }, - "node_modules/sver/node_modules/semver": { + "packages/blockly/node_modules/sver/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "optional": true, "bin": { "semver": "bin/semver.js" } }, - "node_modules/symbol-tree": { + "packages/blockly/node_modules/symbol-tree": { "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" + "license": "MIT" }, - "node_modules/synckit": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", - "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", + "packages/blockly/node_modules/synckit": { + "version": "0.11.8", "dev": true, + "license": "MIT", "dependencies": { - "@pkgr/core": "^0.1.0", - "tslib": "^2.6.2" + "@pkgr/core": "^0.2.4" }, "engines": { "node": "^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/unts" + "url": "https://opencollective.com/synckit" } }, - "node_modules/synckit/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true - }, - "node_modules/tar-fs": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", - "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", + "packages/blockly/node_modules/tar-fs": { + "version": "3.1.1", "dev": true, + "license": "MIT", "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { - "bare-fs": "^2.1.1", - "bare-path": "^2.1.0" + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" } }, - "node_modules/tar-stream": { + "packages/blockly/node_modules/tar-stream": { "version": "3.1.6", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.6.tgz", - "integrity": "sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==", "dev": true, + "license": "MIT", "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, - "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", - "dev": true, - "dependencies": { - "streamx": "^2.12.5" - } - }, - "node_modules/text-decoder": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.0.tgz", - "integrity": "sha512-TmLJNj6UgX8xcUZo4UDStGQtDiTzF7BzWlzn9g7UWrjkpHr5uJTK1ld16wZ3LXb2vb6jH8qU89dW5whuMdXYdw==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4" - } - }, - "node_modules/textextensions": { + "packages/blockly/node_modules/textextensions": { "version": "3.3.0", - "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-3.3.0.tgz", - "integrity": "sha512-mk82dS8eRABNbeVJrEiN5/UMSCliINAuz8mkUwH4SwslkNP//gbEzlWNS5au0z5Dpx40SQxzqZevZkn+WYJ9Dw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -8578,64 +9137,49 @@ "url": "https://bevry.me/fund" } }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true - }, - "node_modules/time-stamp": { + "packages/blockly/node_modules/time-stamp": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", - "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/timers-ext": { + "packages/blockly/node_modules/timers-ext": { "version": "0.1.7", - "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz", - "integrity": "sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==", "dev": true, + "license": "ISC", "dependencies": { "es5-ext": "~0.10.46", "next-tick": "1" } }, - "node_modules/tldts": { - "version": "6.1.48", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.48.tgz", - "integrity": "sha512-SPbnh1zaSzi/OsmHb1vrPNnYuwJbdWjwo5TbBYYMlTtH3/1DSb41t8bcSxkwDmmbG2q6VLPVvQc7Yf23T+1EEw==", + "packages/blockly/node_modules/tldts": { + "version": "6.1.86", + "license": "MIT", "dependencies": { - "tldts-core": "^6.1.48" + "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, - "node_modules/tldts-core": { - "version": "6.1.48", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.48.tgz", - "integrity": "sha512-3gD9iKn/n2UuFH1uilBviK9gvTNT6iYwdqrj1Vr5mh8FuelvpRNaYVH4pNYqUgOGU4aAdL9X35eLuuj0gRsx+A==" + "packages/blockly/node_modules/tldts-core": { + "version": "6.1.86", + "license": "MIT" }, - "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "packages/blockly/node_modules/tmp": { + "version": "0.2.5", "dev": true, - "dependencies": { - "os-tmpdir": "~1.0.2" - }, + "license": "MIT", "engines": { - "node": ">=0.6.0" + "node": ">=14.14" } }, - "node_modules/to-regex-range": { + "packages/blockly/node_modules/to-regex-range": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -8643,11 +9187,10 @@ "node": ">=8.0" } }, - "node_modules/to-through": { + "packages/blockly/node_modules/to-through": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/to-through/-/to-through-3.0.0.tgz", - "integrity": "sha512-y8MN937s/HVhEoBU1SxfHC+wxCHkV1a9gW8eAdTadYh/bGyesZIVcbjI+mSpFbSVwQici/XjBjuUyri1dnXwBw==", "dev": true, + "license": "MIT", "dependencies": { "streamx": "^2.12.5" }, @@ -8655,30 +9198,36 @@ "node": ">=10.13.0" } }, - "node_modules/tough-cookie": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", - "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", + "packages/blockly/node_modules/tough-cookie": { + "version": "5.1.2", + "license": "BSD-3-Clause", "dependencies": { "tldts": "^6.1.32" }, "engines": { - "node": ">=16" + "node": ">=16" + } + }, + "packages/blockly/node_modules/tr46": { + "version": "5.1.0", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" } }, - "node_modules/tree-kill": { + "packages/blockly/node_modules/tree-kill": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true, + "license": "MIT", "bin": { "tree-kill": "cli.js" } }, - "node_modules/ts-api-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", - "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", + "packages/blockly/node_modules/ts-api-utils": { + "version": "2.1.0", "dev": true, "license": "MIT", "engines": { @@ -8688,23 +9237,20 @@ "typescript": ">=4.8.4" } }, - "node_modules/tslib": { + "packages/blockly/node_modules/tslib": { "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true + "dev": true, + "license": "0BSD" }, - "node_modules/type": { + "packages/blockly/node_modules/type": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", - "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", - "dev": true + "dev": true, + "license": "ISC" }, - "node_modules/type-check": { + "packages/blockly/node_modules/type-check": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" }, @@ -8712,20 +9258,18 @@ "node": ">= 0.8.0" } }, - "node_modules/type-detect": { + "packages/blockly/node_modules/type-detect": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, - "node_modules/type-fest": { + "packages/blockly/node_modules/type-fest": { "version": "2.13.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.13.0.tgz", - "integrity": "sha512-lPfAm42MxE4/456+QyIaaVBAwgpJb6xZ8PRu09utnhPdWwcyj9vgy6Sq0Z5yNbJ21EdxB5dRU/Qg8bsyAMtlcw==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=12.20" }, @@ -8733,17 +9277,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/typedarray": { + "packages/blockly/node_modules/typed-query-selector": { + "version": "2.12.0", + "dev": true, + "license": "MIT" + }, + "packages/blockly/node_modules/typedarray": { "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/typescript": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", - "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "packages/blockly/node_modules/typescript": { + "version": "5.9.3", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8752,16 +9299,15 @@ "node": ">=14.17" } }, - "node_modules/typescript-eslint": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.23.0.tgz", - "integrity": "sha512-/LBRo3HrXr5LxmrdYSOCvoAMm7p2jNizNfbIpCgvG4HMsnoprRUOce/+8VJ9BDYWW68rqIENE/haVLWPeFZBVQ==", + "packages/blockly/node_modules/typescript-eslint": { + "version": "8.46.2", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.23.0", - "@typescript-eslint/parser": "8.23.0", - "@typescript-eslint/utils": "8.23.0" + "@typescript-eslint/eslint-plugin": "8.46.2", + "@typescript-eslint/parser": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/utils": "8.46.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -8772,33 +9318,21 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/unbzip2-stream": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", - "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", - "dev": true, - "dependencies": { - "buffer": "^5.2.1", - "through": "^2.3.8" + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/unc-path-regex": { + "packages/blockly/node_modules/unc-path-regex": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", - "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/undertaker": { + "packages/blockly/node_modules/undertaker": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-2.0.0.tgz", - "integrity": "sha512-tO/bf30wBbTsJ7go80j0RzA2rcwX6o7XPBpeFcb+jzoeb4pfMM2zUeSDIkY1AWqeZabWxaQZ/h8N9t35QKDLPQ==", "dev": true, + "license": "MIT", "dependencies": { "bach": "^2.0.1", "fast-levenshtein": "^3.0.0", @@ -8809,43 +9343,37 @@ "node": ">=10.13.0" } }, - "node_modules/undertaker-registry": { + "packages/blockly/node_modules/undertaker-registry": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-2.0.0.tgz", - "integrity": "sha512-+hhVICbnp+rlzZMgxXenpvTxpuvA67Bfgtt+O9WOE5jo7w/dyiF1VmoZVIHvP2EkUjsyKyTwYKlLhA+j47m1Ew==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10.13.0" } }, - "node_modules/undertaker/node_modules/fast-levenshtein": { + "packages/blockly/node_modules/undertaker/node_modules/fast-levenshtein": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", - "integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==", "dev": true, + "license": "MIT", "dependencies": { "fastest-levenshtein": "^1.0.7" } }, - "node_modules/undici": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.0.tgz", - "integrity": "sha512-BUgJXc752Kou3oOIuU1i+yZZypyZRqNPW0vqoMPl8VaoalSfeR0D8/t4iAS3yirs79SSMTxTag+ZC86uswv+Cw==", + "packages/blockly/node_modules/undici": { + "version": "6.21.3", "dev": true, + "license": "MIT", "engines": { "node": ">=18.17" } }, - "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true + "packages/blockly/node_modules/undici-types": { + "version": "6.21.0", + "dev": true, + "license": "MIT" }, - "node_modules/union": { + "packages/blockly/node_modules/union": { "version": "0.5.0", - "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", - "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==", "dev": true, "dependencies": { "qs": "^6.4.0" @@ -8854,83 +9382,81 @@ "node": ">= 0.8.0" } }, - "node_modules/universalify": { + "packages/blockly/node_modules/universalify": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10.0.0" } }, - "node_modules/uri-js": { + "packages/blockly/node_modules/uri-js": { "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } }, - "node_modules/url-join": { + "packages/blockly/node_modules/url-join": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", - "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/urlpattern-polyfill": { + "packages/blockly/node_modules/urlpattern-polyfill": { "version": "10.0.0", - "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", - "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/userhome": { + "packages/blockly/node_modules/userhome": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/userhome/-/userhome-1.0.1.tgz", - "integrity": "sha512-5cnLm4gseXjAclKowC4IjByaGsjtAoV6PrOQOljplNB54ReUYJP8HdAFq2muHinSDAh09PPX/uXDPfdxRHvuSA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8.0" } }, - "node_modules/util-deprecate": { + "packages/blockly/node_modules/util-deprecate": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/uuid": { + "packages/blockly/node_modules/uuid": { "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", "dev": true, + "license": "MIT", "bin": { "uuid": "dist/bin/uuid" } }, - "node_modules/v8flags": { + "packages/blockly/node_modules/v8flags": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-4.0.1.tgz", - "integrity": "sha512-fcRLaS4H/hrZk9hYwbdRM35D0U8IYMfEClhXxCivOojl+yTRAZH3Zy2sSy6qVCiGbV9YAtPssP6jaChqC9vPCg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10.13.0" } }, - "node_modules/value-or-function": { + "packages/blockly/node_modules/validator": { + "version": "13.15.15", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "packages/blockly/node_modules/value-or-function": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-4.0.0.tgz", - "integrity": "sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10.13.0" } }, - "node_modules/vinyl": { + "packages/blockly/node_modules/vinyl": { "version": "2.2.1", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", - "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", "dev": true, + "license": "MIT", "dependencies": { "clone": "^2.1.1", "clone-buffer": "^1.0.0", @@ -8943,11 +9469,10 @@ "node": ">= 0.10" } }, - "node_modules/vinyl-contents": { + "packages/blockly/node_modules/vinyl-contents": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/vinyl-contents/-/vinyl-contents-2.0.0.tgz", - "integrity": "sha512-cHq6NnGyi2pZ7xwdHSW1v4Jfnho4TEGtxZHw01cmnc8+i7jgR6bRnED/LbrKan/Q7CvVLbnvA5OepnhbpjBZ5Q==", "dev": true, + "license": "MIT", "dependencies": { "bl": "^5.0.0", "vinyl": "^3.0.0" @@ -8956,14 +9481,12 @@ "node": ">=10.13.0" } }, - "node_modules/vinyl-contents/node_modules/vinyl": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", - "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", + "packages/blockly/node_modules/vinyl-contents/node_modules/vinyl": { + "version": "3.0.1", "dev": true, + "license": "MIT", "dependencies": { "clone": "^2.1.2", - "clone-stats": "^1.0.0", "remove-trailing-separator": "^1.1.0", "replace-ext": "^2.0.0", "teex": "^1.0.1" @@ -8972,14 +9495,13 @@ "node": ">=10.13.0" } }, - "node_modules/vinyl-fs": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-4.0.0.tgz", - "integrity": "sha512-7GbgBnYfaquMk3Qu9g22x000vbYkOex32930rBnc3qByw6HfMEAoELjCjoJv4HuEQxHAurT+nvMHm6MnJllFLw==", + "packages/blockly/node_modules/vinyl-fs": { + "version": "4.0.2", "dev": true, + "license": "MIT", "dependencies": { "fs-mkdirp-stream": "^2.0.1", - "glob-stream": "^8.0.0", + "glob-stream": "^8.0.3", "graceful-fs": "^4.2.11", "iconv-lite": "^0.6.3", "is-valid-glob": "^1.0.0", @@ -8990,21 +9512,19 @@ "streamx": "^2.14.0", "to-through": "^3.0.0", "value-or-function": "^4.0.0", - "vinyl": "^3.0.0", + "vinyl": "^3.0.1", "vinyl-sourcemap": "^2.0.0" }, "engines": { "node": ">=10.13.0" } }, - "node_modules/vinyl-fs/node_modules/vinyl": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", - "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", + "packages/blockly/node_modules/vinyl-fs/node_modules/vinyl": { + "version": "3.0.1", "dev": true, + "license": "MIT", "dependencies": { "clone": "^2.1.2", - "clone-stats": "^1.0.0", "remove-trailing-separator": "^1.1.0", "replace-ext": "^2.0.0", "teex": "^1.0.1" @@ -9013,11 +9533,10 @@ "node": ">=10.13.0" } }, - "node_modules/vinyl-sourcemap": { + "packages/blockly/node_modules/vinyl-sourcemap": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-2.0.0.tgz", - "integrity": "sha512-BAEvWxbBUXvlNoFQVFVHpybBbjW1r03WhohJzJDSfgrrK5xVYIDTan6xN14DlyImShgDRv2gl9qhM6irVMsV0Q==", "dev": true, + "license": "MIT", "dependencies": { "convert-source-map": "^2.0.0", "graceful-fs": "^4.2.10", @@ -9030,20 +9549,17 @@ "node": ">=10.13.0" } }, - "node_modules/vinyl-sourcemap/node_modules/convert-source-map": { + "packages/blockly/node_modules/vinyl-sourcemap/node_modules/convert-source-map": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/vinyl-sourcemap/node_modules/vinyl": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", - "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", + "packages/blockly/node_modules/vinyl-sourcemap/node_modules/vinyl": { + "version": "3.0.1", "dev": true, + "license": "MIT", "dependencies": { "clone": "^2.1.2", - "clone-stats": "^1.0.0", "remove-trailing-separator": "^1.1.0", "replace-ext": "^2.0.0", "teex": "^1.0.1" @@ -9052,28 +9568,17 @@ "node": ">=10.13.0" } }, - "node_modules/vinyl-sourcemaps-apply": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz", - "integrity": "sha512-+oDh3KYZBoZC8hfocrbrxbLUeaYtQK7J5WU5Br9VqWqmCll3tFJqKp97GC9GmMsVIL0qnx2DgEDVxdo5EZ5sSw==", - "dev": true, - "dependencies": { - "source-map": "^0.5.1" - } - }, - "node_modules/vinyl/node_modules/replace-ext": { + "packages/blockly/node_modules/vinyl/node_modules/replace-ext": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", - "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.10" } }, - "node_modules/w3c-xmlserializer": { + "packages/blockly/node_modules/w3c-xmlserializer": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", - "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "license": "MIT", "dependencies": { "xml-name-validator": "^5.0.0" }, @@ -9081,11 +9586,10 @@ "node": ">=18" } }, - "node_modules/wait-port": { + "packages/blockly/node_modules/wait-port": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-1.1.0.tgz", - "integrity": "sha512-3e04qkoN3LxTMLakdqeWth8nih8usyg+sf1Bgdf9wwUkp05iuK1eSY/QpLvscT/+F/gA89+LpUmmgBtesbqI2Q==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^4.1.2", "commander": "^9.3.0", @@ -9098,28 +9602,26 @@ "node": ">=10" } }, - "node_modules/web-streams-polyfill": { + "packages/blockly/node_modules/web-streams-polyfill": { "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } }, - "node_modules/webdriver": { - "version": "9.4.2", - "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.4.2.tgz", - "integrity": "sha512-1H3DY42kaGYF05sSozwtBpEYxJsnWga2QqYQAPwQmNonqJznGWkl5tlNEuqvI+T7pHTiLbOHlldgdMV5woSrqA==", + "packages/blockly/node_modules/webdriver": { + "version": "9.14.0", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "^20.1.0", "@types/ws": "^8.5.3", - "@wdio/config": "9.4.2", - "@wdio/logger": "9.1.3", - "@wdio/protocols": "9.2.2", - "@wdio/types": "9.4.2", - "@wdio/utils": "9.4.2", + "@wdio/config": "9.14.0", + "@wdio/logger": "9.4.4", + "@wdio/protocols": "9.14.0", + "@wdio/types": "9.14.0", + "@wdio/utils": "9.14.0", "deepmerge-ts": "^7.0.3", "undici": "^6.20.1", "ws": "^8.8.0" @@ -9128,45 +9630,47 @@ "node": ">=18.20.0" } }, - "node_modules/webdriverio": { - "version": "9.4.2", - "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.4.2.tgz", - "integrity": "sha512-e6Gu5pFWQEtJuz3tT0NV3FRhC4/yZuWA8/Q7u1GGC+kaKxZOEuXxNMyrhfSstugGvQ92jnbWP/g2lBwhrZIl2A==", + "packages/blockly/node_modules/webdriver-bidi-protocol": { + "version": "0.2.8", + "dev": true, + "license": "Apache-2.0" + }, + "packages/blockly/node_modules/webdriverio": { + "version": "9.14.0", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "^20.11.30", "@types/sinonjs__fake-timers": "^8.1.5", - "@wdio/config": "9.4.2", - "@wdio/logger": "9.1.3", - "@wdio/protocols": "9.2.2", - "@wdio/repl": "9.0.8", - "@wdio/types": "9.4.2", - "@wdio/utils": "9.4.2", + "@wdio/config": "9.14.0", + "@wdio/logger": "9.4.4", + "@wdio/protocols": "9.14.0", + "@wdio/repl": "9.4.4", + "@wdio/types": "9.14.0", + "@wdio/utils": "9.14.0", "archiver": "^7.0.1", "aria-query": "^5.3.0", "cheerio": "^1.0.0-rc.12", "css-shorthand-properties": "^1.1.1", "css-value": "^0.0.1", "grapheme-splitter": "^1.0.4", - "htmlfy": "^0.3.0", - "import-meta-resolve": "^4.0.0", + "htmlfy": "^0.6.0", "is-plain-obj": "^4.1.0", "jszip": "^3.10.1", "lodash.clonedeep": "^4.5.0", "lodash.zip": "^4.2.0", - "minimatch": "^9.0.3", "query-selector-shadow-dom": "^1.0.1", "resq": "^1.11.0", "rgb2hex": "0.2.5", "serialize-error": "^11.0.3", "urlpattern-polyfill": "^10.0.0", - "webdriver": "9.4.2" + "webdriver": "9.14.0" }, "engines": { "node": ">=18.20.0" }, "peerDependencies": { - "puppeteer-core": "^22.3.0" + "puppeteer-core": ">=22.x || <=24.x" }, "peerDependenciesMeta": { "puppeteer-core": { @@ -9174,20 +9678,10 @@ } } }, - "node_modules/webdriverio/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/webdriverio/node_modules/is-plain-obj": { + "packages/blockly/node_modules/webdriverio/node_modules/is-plain-obj": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -9195,34 +9689,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/webdriverio/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/webidl-conversions": { + "packages/blockly/node_modules/webidl-conversions": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" } }, - "node_modules/whatwg-encoding": { + "packages/blockly/node_modules/whatwg-encoding": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", - "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", "dev": true, + "license": "MIT", "dependencies": { "iconv-lite": "0.6.3" }, @@ -9230,19 +9707,28 @@ "node": ">=12" } }, - "node_modules/whatwg-mimetype": { + "packages/blockly/node_modules/whatwg-mimetype": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "packages/blockly/node_modules/whatwg-url": { + "version": "14.2.0", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, "engines": { "node": ">=18" } }, - "node_modules/which": { + "packages/blockly/node_modules/which": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -9253,34 +9739,14 @@ "node": ">= 8" } }, - "node_modules/workerpool": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", - "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", - "dev": true - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "packages/blockly/node_modules/workerpool": { + "version": "9.3.2", "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } + "license": "Apache-2.0" }, - "node_modules/wrap-ansi-cjs": { + "packages/blockly/node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", "dependencies": { @@ -9295,16 +9761,14 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrappy": { + "packages/blockly/node_modules/wrappy": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "dev": true, + "license": "ISC" }, - "node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "packages/blockly/node_modules/ws": { + "version": "8.18.3", + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -9321,84 +9785,42 @@ } } }, - "node_modules/xml-name-validator": { + "packages/blockly/node_modules/xml-name-validator": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "license": "Apache-2.0", "engines": { "node": ">=18" } }, - "node_modules/xmlchars": { + "packages/blockly/node_modules/xmlchars": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" + "license": "MIT" }, - "node_modules/xtend": { + "packages/blockly/node_modules/xtend": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.4" } }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { + "packages/blockly/node_modules/yallist": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "dev": true, + "license": "ISC" }, - "node_modules/yaml": { + "packages/blockly/node_modules/yaml": { "version": "2.2.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz", - "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==", "dev": true, + "license": "ISC", "engines": { "node": ">= 14" } }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-unparser": { + "packages/blockly/node_modules/yargs-unparser": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", - "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", "dev": true, + "license": "MIT", "dependencies": { "camelcase": "^6.0.0", "decamelize": "^4.0.0", @@ -9409,11 +9831,10 @@ "node": ">=10" } }, - "node_modules/yargs-unparser/node_modules/camelcase": { + "packages/blockly/node_modules/yargs-unparser/node_modules/camelcase": { "version": "6.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.1.tgz", - "integrity": "sha512-tVI4q5jjFV5CavAU8DXfza/TJcZutVKo/5Foskmsqcm0MsL91moHvwiGNnqaa2o6PF/7yT5ikDRcVcl8Rj6LCA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -9421,11 +9842,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/yargs-unparser/node_modules/decamelize": { + "packages/blockly/node_modules/yargs-unparser/node_modules/decamelize": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -9433,44 +9853,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/yargs-unparser/node_modules/is-plain-obj": { + "packages/blockly/node_modules/yargs-unparser/node_modules/is-plain-obj": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/yargs/node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { + "packages/blockly/node_modules/yauzl": { "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", "dev": true, + "license": "MIT", "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, - "node_modules/yocto-queue": { + "packages/blockly/node_modules/yocto-queue": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -9478,11 +9881,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/zip-stream": { + "packages/blockly/node_modules/z-schema": { + "version": "5.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^9.4.1" + } + }, + "packages/blockly/node_modules/zip-stream": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", - "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", "dev": true, + "license": "MIT", "dependencies": { "archiver-utils": "^5.0.0", "compress-commons": "^6.0.2", @@ -9492,35 +9913,10 @@ "node": ">= 14" } }, - "node_modules/zip-stream/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/zip-stream/node_modules/readable-stream": { + "packages/blockly/node_modules/zip-stream/node_modules/readable-stream": { "version": "4.5.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", - "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", "dev": true, + "license": "MIT", "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", @@ -9532,10 +9928,8 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/zip-stream/node_modules/safe-buffer": { + "packages/blockly/node_modules/zip-stream/node_modules/safe-buffer": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true, "funding": [ { @@ -9550,16 +9944,24 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, - "node_modules/zip-stream/node_modules/string_decoder": { + "packages/blockly/node_modules/zip-stream/node_modules/string_decoder": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" } + }, + "packages/blockly/node_modules/zod": { + "version": "3.25.76", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index c885cd496bf..a7f21fcca6b 100644 --- a/package.json +++ b/package.json @@ -1,152 +1,32 @@ { - "name": "blockly", - "version": "11.2.2-mit-appinventor.1", - "description": "Blockly is a library for building visual programming editors.", + "name": "blockly-repo", + "version": "0.0.0", + "description": "Monorepo for blockly and related packages", "keywords": [ "blockly" ], - "repository": { - "type": "git", - "url": "git+https://github.com/google/blockly.git" - }, + "homepage": "https://blockly.com", "bugs": { - "url": "https://github.com/google/blockly/issues" - }, - "homepage": "https://developers.google.com/blockly/", - "author": { - "name": "Neil Fraser" - }, - "scripts": { - "build": "gulp build", - "build-debug": "gulp build --verbose --debug", - "build-debug-log": "npm run build:debug > build-debug.log 2>&1 && tail -3 build-debug.log", - "build-strict": "gulp build --verbose --strict", - "build-strict-log": "npm run build:strict > build-debug.log 2>&1 && tail -3 build-debug.log", - "clean": "gulp clean", - "deployDemos": "npm ci && gulp deployDemos", - "deployDemos:beta": "npm ci && gulp deployDemosBeta", - "docs": "gulp docs", - "format": "prettier --write .", - "format:check": "prettier --check .", - "messages": "gulp messages", - "lint": "eslint .", - "lint:fix": "eslint . --fix", - "langfiles": "gulp langfiles", - "minify": "gulp minify", - "package": "gulp package", - "postinstall": "patch-package", - "prepareDemos": "gulp prepareDemos", - "publish": "npm ci && gulp publish", - "publish:beta": "npm ci && gulp publishBeta", - "recompile": "gulp recompile", - "release": "gulp gitCreateRC", - "start": "npm run build && concurrently -n tsc,server \"tsc --watch --preserveWatchOutput --outDir \"build/src\" --declarationDir \"build/declarations\"\" \"http-server ./ -s -o /tests/playground.html -c-1\"", - "tsc": "gulp tsc", - "test": "gulp test", - "test:browser": "cd tests/browser && npx mocha", - "test:generators": "gulp testGenerators", - "test:mocha:interactive": "npm run build && concurrently -n tsc,server \"tsc --watch --preserveWatchOutput --outDir \"build/src\" --declarationDir \"build/declarations\"\" \"http-server ./ -o /tests/mocha/index.html -c-1\"", - "test:compile:advanced": "gulp buildAdvancedCompilationTest --debug", - "updateGithubPages": "npm ci && gulp gitUpdateGithubPages" + "url": "https://github.com/RaspberryPiFoundation/blockly/issues" }, - "exports": { - ".": { - "types": "./index.d.ts", - "import": "./index.mjs", - "umd": "./blockly.min.js", - "default": "./index.js" - }, - "./core": { - "types": "./core.d.ts", - "node": "./core-node.js", - "import": "./blockly.mjs", - "default": "./blockly_compressed.js" - }, - "./blocks": { - "types": "./blocks.d.ts", - "import": "./blocks.mjs", - "default": "./blocks_compressed.js" - }, - "./dart": { - "types": "./dart.d.ts", - "import": "./dart.mjs", - "default": "./dart_compressed.js" - }, - "./lua": { - "types": "./lua.d.ts", - "import": "./lua.mjs", - "default": "./lua_compressed.js" - }, - "./javascript": { - "types": "./javascript.d.ts", - "import": "./javascript.mjs", - "default": "./javascript_compressed.js" - }, - "./php": { - "types": "./php.d.ts", - "import": "./php.mjs", - "default": "./php_compressed.js" - }, - "./python": { - "types": "./python.d.ts", - "import": "./python.mjs", - "default": "./python_compressed.js" - }, - "./msg/*": { - "types": "./msg/*.d.ts", - "import": "./msg/*.mjs", - "default": "./msg/*.js" - } + "repository": { + "type": "git", + "url": "git+https://github.com/RaspberryPiFoundation/blockly.git" }, "license": "Apache-2.0", + "author": "Raspberry Pi Foundation", + "private": true, + "workspaces": [ + "packages/*" + ], "devDependencies": { - "@blockly/block-test": "^6.0.4", - "@blockly/dev-tools": "^8.0.6", - "@blockly/theme-modern": "^6.0.3", - "@hyperjump/browser": "^1.1.4", - "@hyperjump/json-schema": "^1.5.0", - "@microsoft/api-documenter": "^7.22.4", - "@microsoft/api-extractor": "^7.29.5", - "async-done": "^2.0.0", - "chai": "^5.1.1", - "concurrently": "^9.0.1", - "eslint": "^9.15.0", - "eslint-config-google": "^0.14.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-jsdoc": "^50.5.0", - "eslint-plugin-prettier": "^5.2.1", - "glob": "^11.0.1", - "globals": "^15.12.0", - "google-closure-compiler": "^20240317.0.0", - "gulp": "^5.0.0", - "gulp-concat": "^2.6.1", - "gulp-gzip": "^1.4.2", - "gulp-header": "^2.0.9", - "gulp-insert": "^0.5.0", - "gulp-rename": "^2.0.0", - "gulp-replace": "^1.0.0", - "gulp-series": "^1.0.2", - "gulp-shell": "^0.8.0", - "gulp-sourcemaps": "^3.0.0", - "gulp-umd": "^2.0.0", - "http-server": "^14.0.0", - "json5": "^2.2.0", - "markdown-tables-to-json": "^0.1.7", - "mocha": "^10.0.0", - "patch-package": "^8.0.0", - "prettier": "^3.3.3", - "prettier-plugin-organize-imports": "^4.0.0", - "readline-sync": "^1.4.10", - "rimraf": "^5.0.0", - "typescript": "^5.3.3", - "typescript-eslint": "^8.16.0", - "webdriverio": "^9.0.7", - "yargs": "^17.2.1" + "@commitlint/cli": "^20.1.0", + "@commitlint/config-conventional": "^20.0.0" }, - "dependencies": { - "jsdom": "25.0.1" - }, - "engines": { - "node": ">=18" + "scripts": { + "test": "npm run test --ws --if-present", + "lint": "npm run lint --ws --if-present", + "build": "npm run build --ws --if-present", + "format:check": "npm run format:check --ws --if-present" } } diff --git a/packages/blockly/.gitignore b/packages/blockly/.gitignore new file mode 100644 index 00000000000..e3c367e823a --- /dev/null +++ b/packages/blockly/.gitignore @@ -0,0 +1,10 @@ +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/.prettierignore b/packages/blockly/.prettierignore similarity index 100% rename from .prettierignore rename to packages/blockly/.prettierignore diff --git a/.prettierrc.js b/packages/blockly/.prettierrc.js similarity index 100% rename from .prettierrc.js rename to packages/blockly/.prettierrc.js diff --git a/api-extractor.json b/packages/blockly/api-extractor.json similarity index 99% rename from api-extractor.json rename to packages/blockly/api-extractor.json index 6c599d255b4..66414503a29 100644 --- a/api-extractor.json +++ b/packages/blockly/api-extractor.json @@ -352,6 +352,11 @@ // Needs investigation. "ae-forgotten-export": { "logLevel": "none" + }, + + // We don't prefix our internal APIs with underscores. + "ae-internal-missing-underscore": { + "logLevel": "none" } }, diff --git a/appengine/.gcloudignore b/packages/blockly/appengine/.gcloudignore similarity index 100% rename from appengine/.gcloudignore rename to packages/blockly/appengine/.gcloudignore diff --git a/appengine/README.txt b/packages/blockly/appengine/README.txt similarity index 100% rename from appengine/README.txt rename to packages/blockly/appengine/README.txt diff --git a/appengine/add_timestamps.py b/packages/blockly/appengine/add_timestamps.py similarity index 100% rename from appengine/add_timestamps.py rename to packages/blockly/appengine/add_timestamps.py diff --git a/appengine/app.yaml b/packages/blockly/appengine/app.yaml similarity index 98% rename from appengine/app.yaml rename to packages/blockly/appengine/app.yaml index 9c93fb6873d..563af8b24d0 100644 --- a/appengine/app.yaml +++ b/packages/blockly/appengine/app.yaml @@ -70,6 +70,8 @@ handlers: # Blockly files. - url: /static static_dir: static + http_headers: + Access-Control-Allow-Origin: "*" secure: always # Storage API. diff --git a/appengine/apple-touch-icon.png b/packages/blockly/appengine/apple-touch-icon.png similarity index 100% rename from appengine/apple-touch-icon.png rename to packages/blockly/appengine/apple-touch-icon.png diff --git a/appengine/blockly_compressed.js b/packages/blockly/appengine/blockly_compressed.js similarity index 100% rename from appengine/blockly_compressed.js rename to packages/blockly/appengine/blockly_compressed.js diff --git a/appengine/expiration.py b/packages/blockly/appengine/expiration.py similarity index 100% rename from appengine/expiration.py rename to packages/blockly/appengine/expiration.py diff --git a/appengine/favicon.ico b/packages/blockly/appengine/favicon.ico similarity index 100% rename from appengine/favicon.ico rename to packages/blockly/appengine/favicon.ico diff --git a/appengine/index.yaml b/packages/blockly/appengine/index.yaml similarity index 100% rename from appengine/index.yaml rename to packages/blockly/appengine/index.yaml diff --git a/appengine/main.py b/packages/blockly/appengine/main.py similarity index 100% rename from appengine/main.py rename to packages/blockly/appengine/main.py diff --git a/appengine/redirect.html b/packages/blockly/appengine/redirect.html similarity index 100% rename from appengine/redirect.html rename to packages/blockly/appengine/redirect.html diff --git a/appengine/requirements.txt b/packages/blockly/appengine/requirements.txt similarity index 100% rename from appengine/requirements.txt rename to packages/blockly/appengine/requirements.txt diff --git a/appengine/robots.txt b/packages/blockly/appengine/robots.txt similarity index 100% rename from appengine/robots.txt rename to packages/blockly/appengine/robots.txt diff --git a/appengine/storage.js b/packages/blockly/appengine/storage.js similarity index 100% rename from appengine/storage.js rename to packages/blockly/appengine/storage.js diff --git a/appengine/storage.py b/packages/blockly/appengine/storage.py similarity index 100% rename from appengine/storage.py rename to packages/blockly/appengine/storage.py diff --git a/blocks/blocks.ts b/packages/blockly/blocks/blocks.ts similarity index 100% rename from blocks/blocks.ts rename to packages/blockly/blocks/blocks.ts diff --git a/blocks/lists.ts b/packages/blockly/blocks/lists.ts similarity index 100% rename from blocks/lists.ts rename to packages/blockly/blocks/lists.ts diff --git a/blocks/logic.ts b/packages/blockly/blocks/logic.ts similarity index 100% rename from blocks/logic.ts rename to packages/blockly/blocks/logic.ts diff --git a/blocks/loops.ts b/packages/blockly/blocks/loops.ts similarity index 99% rename from blocks/loops.ts rename to packages/blockly/blocks/loops.ts index dd5a8116211..6d450e53215 100644 --- a/blocks/loops.ts +++ b/packages/blockly/blocks/loops.ts @@ -269,7 +269,7 @@ const CUSTOM_CONTEXT_MENU_CREATE_VARIABLES_GET_MIXIN = { } const varField = this.getField('VAR') as FieldVariable; const variable = varField.getVariable()!; - const varName = variable.name; + const varName = variable.getName(); if (!this.isCollapsed() && varName !== null) { const getVarBlockState = { type: 'variables_get', diff --git a/blocks/math.ts b/packages/blockly/blocks/math.ts similarity index 100% rename from blocks/math.ts rename to packages/blockly/blocks/math.ts diff --git a/blocks/procedures.ts b/packages/blockly/blocks/procedures.ts similarity index 92% rename from blocks/procedures.ts rename to packages/blockly/blocks/procedures.ts index 20d8fa36bb0..9493a2dc035 100644 --- a/blocks/procedures.ts +++ b/packages/blockly/blocks/procedures.ts @@ -9,7 +9,6 @@ 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'; @@ -27,15 +26,19 @@ 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 {getFocusManager} from '../core/focus_manager.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 type { + IVariableModel, + IVariableState, +} from '../core/interfaces/i_variable_model.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'; @@ -48,7 +51,7 @@ export const blocks: {[key: string]: BlockDefinition} = {}; type ProcedureBlock = Block & ProcedureMixin; interface ProcedureMixin extends ProcedureMixinType { arguments_: string[]; - argumentVarModels_: VariableModel[]; + argumentVarModels_: IVariableModel[]; callType_: string; paramIds_: string[]; hasStatements_: boolean; @@ -128,7 +131,7 @@ const PROCEDURE_DEF_COMMON = { 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('name', argModel.getName()); parameter.setAttribute('varid', argModel.getId()); if (opt_paramIds && this.paramIds_) { parameter.setAttribute('paramId', this.paramIds_[i]); @@ -196,7 +199,7 @@ const PROCEDURE_DEF_COMMON = { 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, + 'name': this.argumentVarModels_[i].getName(), 'id': this.argumentVarModels_[i].getId(), }); } @@ -224,7 +227,7 @@ const PROCEDURE_DEF_COMMON = { param['name'], '', ); - this.arguments_.push(variable.name); + this.arguments_.push(variable.getName()); this.argumentVarModels_.push(variable); } } @@ -305,7 +308,9 @@ const PROCEDURE_DEF_COMMON = { while (paramBlock && !paramBlock.isInsertionMarker()) { const varName = paramBlock.getFieldValue('NAME'); this.arguments_.push(varName); - const variable = this.workspace.getVariable(varName, '')!; + const variable = this.workspace + .getVariableMap() + .getVariable(varName, '')!; this.argumentVarModels_.push(variable); this.paramIds_.push(paramBlock.id); @@ -352,7 +357,9 @@ const PROCEDURE_DEF_COMMON = { * * @returns List of variable models. */ - getVarModels: function (this: ProcedureBlock): VariableModel[] { + getVarModels: function ( + this: ProcedureBlock, + ): IVariableModel[] { return this.argumentVarModels_; }, /** @@ -369,24 +376,24 @@ const PROCEDURE_DEF_COMMON = { oldId: string, newId: string, ) { - const oldVariable = this.workspace.getVariableById(oldId)!; - if (oldVariable.type !== '') { + const oldVariable = this.workspace.getVariableMap().getVariableById(oldId)!; + if (oldVariable.getType() !== '') { // Procedure arguments always have the empty type. return; } - const oldName = oldVariable.name; - const newVar = this.workspace.getVariableById(newId)!; + const oldName = oldVariable.getName(); + const newVar = this.workspace.getVariableMap().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.arguments_[i] = newVar.getName(); this.argumentVarModels_[i] = newVar; change = true; } } if (change) { - this.displayRenamedVar_(oldName, newVar.name); + this.displayRenamedVar_(oldName, newVar.getName()); Procedures.mutateCallers(this); } }, @@ -398,9 +405,9 @@ const PROCEDURE_DEF_COMMON = { */ updateVarName: function ( this: ProcedureBlock & BlockSvg, - variable: VariableModel, + variable: IVariableModel, ) { - const newName = variable.name; + const newName = variable.getName(); let change = false; let oldName; for (let i = 0; i < this.argumentVarModels_.length; i++) { @@ -457,7 +464,7 @@ const PROCEDURE_DEF_COMMON = { // Add option to create caller. const name = this.getFieldValue('NAME'); const callProcedureBlockState = { - type: (this as AnyDuringMigration).callType_, + type: this.callType_, extraState: {name: name, params: this.arguments_}, }; options.push({ @@ -473,12 +480,16 @@ const PROCEDURE_DEF_COMMON = { const getVarBlockState = { type: 'variables_get', fields: { - VAR: {name: argVar.name, id: argVar.getId(), type: argVar.type}, + VAR: { + name: argVar.getName(), + id: argVar.getId(), + type: argVar.getType(), + }, }, }; options.push({ enabled: true, - text: Msg['VARIABLES_SET_CREATE_GET'].replace('%1', argVar.name), + text: Msg['VARIABLES_SET_CREATE_GET'].replace('%1', argVar.getName()), callback: ContextMenu.callbackFactory(this, getVarBlockState), }); } @@ -620,30 +631,49 @@ 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[]; -}; +/** + * Field responsible for editing procedure argument names. + */ +class ProcedureArgumentField extends FieldTextInput { + /** + * Whether or not this field is currently being edited interactively. + */ + editingInteractively = false; + + /** + * The procedure argument variable whose name is being interactively edited. + */ + editingVariable?: IVariableModel; + + /** + * Displays the field editor. + * + * @param e The event that triggered display of the field editor. + */ + protected override showEditor_(e?: Event) { + super.showEditor_(e); + this.editingInteractively = true; + this.editingVariable = undefined; + } + + /** + * Handles cleanup when the field editor is dismissed. + */ + override onFinishEditing_(value: string) { + super.onFinishEditing_(value); + this.editingInteractively = false; + } +} 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; + const field = new ProcedureArgumentField( + Procedures.DEFAULT_ARG, + this.validator_, + ); this.appendDummyInput() .appendField(Msg['PROCEDURES_MUTATORARG_TITLE']) @@ -653,14 +683,6 @@ const PROCEDURES_MUTATORARGUMENT = { 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'); }, /** @@ -674,11 +696,11 @@ const PROCEDURES_MUTATORARGUMENT = { * @returns Valid name, or null if a name was not specified. */ validator_: function ( - this: FieldTextInputForArgument, + this: ProcedureArgumentField, varName: string, ): string | null { const sourceBlock = this.getSourceBlock()!; - const outerWs = sourceBlock!.workspace.getRootWorkspace()!; + const outerWs = sourceBlock.workspace.getRootWorkspace()!; varName = varName.replace(/[\s\xa0]+/g, ' ').replace(/^ | $/g, ''); if (!varName) { return null; @@ -706,51 +728,32 @@ const PROCEDURES_MUTATORARGUMENT = { if (sourceBlock.isInFlyout) { return varName; } - - let model = outerWs.getVariable(varName, ''); - if (model && model.name !== varName) { + const variableMap = outerWs.getVariableMap(); + const model = variableMap.getVariable(varName, ''); + if (model && model.getName() !== varName) { // Rename the variable (case change) - outerWs.renameVariableById(model.getId(), varName); + variableMap.renameVariable(model, varName); } if (!model) { - model = outerWs.createVariable(varName, ''); - if (model && this.createdVariables_) { - this.createdVariables_.push(model); + if (this.editingInteractively) { + if (!this.editingVariable) { + this.editingVariable = variableMap.createVariable(varName, ''); + } else { + variableMap.renameVariable(this.editingVariable, varName); + } + } else { + variableMap.createVariable(varName, ''); } } 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[]; + argumentVarModels_: IVariableModel[]; arguments_: string[]; defType_: string; quarkIds_: string[] | null; @@ -1029,7 +1032,7 @@ const PROCEDURE_CALL_COMMON = { * * @returns List of variable models. */ - getVarModels: function (this: CallBlock): VariableModel[] { + getVarModels: function (this: CallBlock): IVariableModel[] { return this.argumentVarModels_; }, /** @@ -1177,7 +1180,7 @@ const PROCEDURE_CALL_COMMON = { const def = Procedures.getDefinition(name, workspace); if (def) { (workspace as WorkspaceSvg).centerOnBlock(def.id); - common.setSelected(def as BlockSvg); + getFocusManager().focusNode(def as BlockSvg); } }, }); diff --git a/blocks/text.ts b/packages/blockly/blocks/text.ts similarity index 100% rename from blocks/text.ts rename to packages/blockly/blocks/text.ts diff --git a/blocks/variables.ts b/packages/blockly/blocks/variables.ts similarity index 95% rename from blocks/variables.ts rename to packages/blockly/blocks/variables.ts index 0ec9112a3d6..4f1f640fa81 100644 --- a/blocks/variables.ts +++ b/packages/blockly/blocks/variables.ts @@ -21,7 +21,6 @@ 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. @@ -165,11 +164,11 @@ 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(); + const variable = variableField.getVariable(); + if (variable) { + Variables.deleteVariable(variable.getWorkspace(), variable, block); + } }; }; diff --git a/blocks/variables_dynamic.ts b/packages/blockly/blocks/variables_dynamic.ts similarity index 94% rename from blocks/variables_dynamic.ts rename to packages/blockly/blocks/variables_dynamic.ts index 8e4ce290e09..8afd24cf2e3 100644 --- a/blocks/variables_dynamic.ts +++ b/packages/blockly/blocks/variables_dynamic.ts @@ -22,7 +22,6 @@ 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. @@ -144,9 +143,9 @@ const CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN = { const id = this.getFieldValue('VAR'); const variableModel = Variables.getVariable(this.workspace, id)!; if (this.type === 'variables_get_dynamic') { - this.outputConnection!.setCheck(variableModel.type); + this.outputConnection!.setCheck(variableModel.getType()); } else { - this.getInput('VALUE')!.connection!.setCheck(variableModel.type); + this.getInput('VALUE')!.connection!.setCheck(variableModel.getType()); } }, }; @@ -176,11 +175,11 @@ const renameOptionCallbackFactory = function (block: VariableBlock) { */ 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(); + const variable = variableField.getVariable(); + if (variable) { + Variables.deleteVariable(variable.getWorkspace(), variable, block); + } }; }; diff --git a/core/any_aliases.ts b/packages/blockly/core/any_aliases.ts similarity index 100% rename from core/any_aliases.ts rename to packages/blockly/core/any_aliases.ts diff --git a/core/block.ts b/packages/blockly/core/block.ts similarity index 94% rename from core/block.ts rename to packages/blockly/core/block.ts index 0face8f8c9b..4c8f8084202 100644 --- a/core/block.ts +++ b/packages/blockly/core/block.ts @@ -40,25 +40,26 @@ 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 type { + IVariableModel, + IVariableState, +} from './interfaces/i_variable_model.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 { +export class Block { /** * An optional callback method to use whenever the block's parent workspace * changes. This is usually only called from the constructor, the block type @@ -500,22 +501,32 @@ export class Block implements IASTNodeLocation { // 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!); - } + + if (!opt_healStack) return; + + // Immovable or shadow next blocks need to move along with the block; keep + // going until we encounter a normal block or run off the end of the stack. + let nextBlock = this.getNextBlock(); + while (nextBlock && (nextBlock.isShadow() || !nextBlock.isMovable())) { + nextBlock = nextBlock.getNextBlock(); + } + if (!nextBlock) return; + + // Disconnect the next statement. + const nextTarget = + nextBlock.previousConnection?.targetBlock()?.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!); } } @@ -790,9 +801,10 @@ export class Block implements IASTNodeLocation { isDeletable(): boolean { return ( this.deletable && + !this.isInFlyout && !this.shadow && !this.isDeadOrDying() && - !this.workspace.options.readOnly + !this.workspace.isReadOnly() ); } @@ -823,9 +835,10 @@ export class Block implements IASTNodeLocation { isMovable(): boolean { return ( this.movable && + !this.isInFlyout && !this.shadow && !this.isDeadOrDying() && - !this.workspace.options.readOnly + !this.workspace.isReadOnly() ); } @@ -914,7 +927,7 @@ export class Block implements IASTNodeLocation { */ isEditable(): boolean { return ( - this.editable && !this.isDeadOrDying() && !this.workspace.options.readOnly + this.editable && !this.isDeadOrDying() && !this.workspace.isReadOnly() ); } @@ -934,10 +947,8 @@ export class Block implements IASTNodeLocation { */ 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(); - } + for (const field of this.getFields()) { + field.updateEditable(); } } @@ -1104,16 +1115,27 @@ export class Block implements IASTNodeLocation { ' 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; - } + for (const field of this.getFields()) { + if (field.name === name) { + return field; } } return null; } + /** + * Returns a generator that provides every field on the block. + * + * @returns A generator that can be used to iterate the fields on the block. + */ + *getFields(): Generator { + for (const input of this.inputList) { + for (const field of input.fieldRow) { + yield field; + } + } + } + /** * Return all variables referenced by this block. * @@ -1121,12 +1143,9 @@ export class Block implements IASTNodeLocation { */ 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); - } + for (const field of this.getFields()) { + if (field.referencesVariables()) { + vars.push(field.getValue()); } } return vars; @@ -1138,19 +1157,17 @@ export class Block implements IASTNodeLocation { * @returns List of variable models. * @internal */ - getVarModels(): VariableModel[] { + getVarModels(): IVariableModel[] { 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); - } + for (const field of this.getFields()) { + if (field.referencesVariables()) { + const model = this.workspace + .getVariableMap() + .getVariableById(field.getValue() as string); + // Check if the variable actually exists (and isn't just a potential + // variable). + if (model) { + vars.push(model); } } } @@ -1164,15 +1181,13 @@ export class Block implements IASTNodeLocation { * @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(); - } + updateVarName(variable: IVariableModel) { + for (const field of this.getFields()) { + if ( + field.referencesVariables() && + variable.getId() === field.getValue() + ) { + field.refreshVariableName(); } } } @@ -1186,11 +1201,9 @@ export class Block implements IASTNodeLocation { * 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); - } + for (const field of this.getFields()) { + if (field.referencesVariables() && oldId === field.getValue()) { + field.setValue(newId); } } } @@ -1408,48 +1421,6 @@ export class Block implements IASTNodeLocation { 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 @@ -2516,7 +2487,7 @@ export class Block implements IASTNodeLocation { * * 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}. + * and child blocks), use {@link (Block:class).toString | toString()}. * * @returns The description. */ diff --git a/core/block_animations.ts b/packages/blockly/core/block_animations.ts similarity index 100% rename from core/block_animations.ts rename to packages/blockly/core/block_animations.ts diff --git a/packages/blockly/core/block_flyout_inflater.ts b/packages/blockly/core/block_flyout_inflater.ts new file mode 100644 index 00000000000..80f86855182 --- /dev/null +++ b/packages/blockly/core/block_flyout_inflater.ts @@ -0,0 +1,283 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {BlockSvg} from './block_svg.js'; +import * as browserEvents from './browser_events.js'; +import * as common from './common.js'; +import {MANUALLY_DISABLED} from './constants.js'; +import type {Abstract as AbstractEvent} from './events/events_abstract.js'; +import {EventType} from './events/type.js'; +import {FlyoutItem} from './flyout_item.js'; +import type {IFlyout} from './interfaces/i_flyout.js'; +import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js'; +import * as registry from './registry.js'; +import * as blocks from './serialization/blocks.js'; +import type {BlockInfo} from './utils/toolbox.js'; +import * as utilsXml from './utils/xml.js'; +import type {WorkspaceSvg} from './workspace_svg.js'; +import * as Xml from './xml.js'; + +/** + * 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'; + +const BLOCK_TYPE = 'block'; + +/** + * Class responsible for creating blocks for flyouts. + */ +export class BlockFlyoutInflater implements IFlyoutInflater { + protected permanentlyDisabledBlocks = new Set(); + protected listeners = new Map(); + protected flyout?: IFlyout; + private capacityWrapper: (event: AbstractEvent) => void; + + /** + * Creates a new BlockFlyoutInflater instance. + */ + constructor() { + this.capacityWrapper = this.filterFlyoutBasedOnCapacity.bind(this); + } + + /** + * Inflates a flyout block from the given state and adds it to the flyout. + * + * @param state A JSON representation of a flyout block. + * @param flyout The flyout to create the block on. + * @returns A newly created block. + */ + load(state: object, flyout: IFlyout): FlyoutItem { + this.setFlyout(flyout); + const block = this.createBlock(state as BlockInfo, flyout.getWorkspace()); + + if (!block.isEnabled()) { + // Record blocks that were initially disabled. + // Do not enable these blocks as a result of capacity filtering. + this.permanentlyDisabledBlocks.add(block); + } else { + this.updateStateBasedOnCapacity(block); + } + + // 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. + block.getDescendants(false).forEach((b) => (b.isInFlyout = true)); + this.addBlockListeners(block); + + return new FlyoutItem(block, BLOCK_TYPE); + } + + /** + * Creates a block on the given workspace. + * + * @param blockDefinition A JSON representation of the block to create. + * @param workspace The workspace to create the block on. + * @returns The newly created block. + */ + createBlock(blockDefinition: BlockInfo, workspace: WorkspaceSvg): BlockSvg { + let block; + if (blockDefinition['blockxml']) { + const xml = ( + typeof blockDefinition['blockxml'] === 'string' + ? utilsXml.textToDom(blockDefinition['blockxml']) + : blockDefinition['blockxml'] + ) as Element; + block = Xml.domToBlockInternal(xml, workspace); + } else { + if (blockDefinition['enabled'] === undefined) { + blockDefinition['enabled'] = + blockDefinition['disabled'] !== 'true' && + blockDefinition['disabled'] !== true; + } + if ( + blockDefinition['disabledReasons'] === undefined && + blockDefinition['enabled'] === false + ) { + blockDefinition['disabledReasons'] = [MANUALLY_DISABLED]; + } + // These fields used to be allowed and may still be present, but are + // ignored here since everything in the flyout should always be laid out + // linearly. + if ('x' in blockDefinition) { + delete blockDefinition['x']; + } + if ('y' in blockDefinition) { + delete blockDefinition['y']; + } + block = blocks.appendInternal(blockDefinition as blocks.State, workspace); + } + + return block as BlockSvg; + } + + /** + * Returns the amount of space that should follow this block. + * + * @param state A JSON representation of a flyout block. + * @param defaultGap The default spacing for flyout items. + * @returns The amount of space that should follow this block. + */ + gapForItem(state: object, defaultGap: number): number { + const blockState = state as BlockInfo; + let gap; + if (blockState['gap']) { + gap = parseInt(String(blockState['gap'])); + } else if (blockState['blockxml']) { + const xml = ( + typeof blockState['blockxml'] === 'string' + ? utilsXml.textToDom(blockState['blockxml']) + : blockState['blockxml'] + ) as Element; + gap = parseInt(xml.getAttribute('gap')!); + } + + return !gap || isNaN(gap) ? defaultGap : gap; + } + + /** + * Disposes of the given block. + * + * @param item The flyout block to dispose of. + */ + disposeItem(item: FlyoutItem): void { + const element = item.getElement(); + if (!(element instanceof BlockSvg)) return; + this.removeListeners(element.id); + element.dispose(false, false); + } + + /** + * Removes event listeners for the block with the given ID. + * + * @param blockId The ID of the block to remove event listeners from. + */ + protected removeListeners(blockId: string) { + const blockListeners = this.listeners.get(blockId) ?? []; + blockListeners.forEach((l) => browserEvents.unbind(l)); + this.listeners.delete(blockId); + } + + /** + * Updates this inflater's flyout. + * + * @param flyout The flyout that owns this inflater. + */ + protected setFlyout(flyout: IFlyout) { + if (this.flyout === flyout) return; + + if (this.flyout) { + this.flyout.targetWorkspace?.removeChangeListener(this.capacityWrapper); + } + this.flyout = flyout; + this.flyout.targetWorkspace?.addChangeListener(this.capacityWrapper); + } + + /** + * Updates the enabled state of the given block based on the capacity of the + * workspace. + * + * @param block The block to update the enabled/disabled state of. + */ + private updateStateBasedOnCapacity(block: BlockSvg) { + const enable = this.flyout?.targetWorkspace?.isCapacityAvailable( + common.getBlockTypeCounts(block), + ); + let currentBlock: BlockSvg | null = block; + while (currentBlock) { + currentBlock.setDisabledReason( + !enable, + WORKSPACE_AT_BLOCK_CAPACITY_DISABLED_REASON, + ); + currentBlock = currentBlock.getNextBlock(); + } + } + + /** + * Add listeners to a block that has been added to the flyout. + * + * @param block The block to add listeners for. + */ + protected addBlockListeners(block: BlockSvg) { + const blockListeners = []; + + blockListeners.push( + browserEvents.conditionalBind( + block.getSvgRoot(), + 'pointerdown', + block, + (e: PointerEvent) => { + const gesture = this.flyout?.targetWorkspace?.getGesture(e); + if (gesture && this.flyout) { + gesture.setStartBlock(block); + gesture.handleFlyoutStart(e, this.flyout); + } + }, + ), + ); + + blockListeners.push( + browserEvents.bind(block.getSvgRoot(), 'pointermove', null, () => { + if (!this.flyout?.targetWorkspace?.isDragging()) { + block.addSelect(); + } + }), + ); + blockListeners.push( + browserEvents.bind(block.getSvgRoot(), 'pointerleave', null, () => { + if (!this.flyout?.targetWorkspace?.isDragging()) { + block.removeSelect(); + } + }), + ); + + this.listeners.set(block.id, blockListeners); + } + + /** + * Updates the state of blocks in our owning flyout to be disabled/enabled + * based on the capacity of the workspace for more blocks of that type. + * + * @param event The event that triggered this update. + */ + private filterFlyoutBasedOnCapacity(event: AbstractEvent) { + if ( + !this.flyout || + (event && + !( + event.type === EventType.BLOCK_CREATE || + event.type === EventType.BLOCK_DELETE + )) + ) + return; + + this.flyout + .getWorkspace() + .getTopBlocks(false) + .forEach((block) => { + if (!this.permanentlyDisabledBlocks.has(block)) { + this.updateStateBasedOnCapacity(block); + } + }); + } + + /** + * Returns the type of items this inflater is responsible for creating. + * + * @returns An identifier for the type of items this inflater creates. + */ + getType() { + return BLOCK_TYPE; + } +} + +registry.register( + registry.Type.FLYOUT_INFLATER, + BLOCK_TYPE, + BlockFlyoutInflater, +); diff --git a/core/block_svg.ts b/packages/blockly/core/block_svg.ts similarity index 83% rename from core/block_svg.ts rename to packages/blockly/core/block_svg.ts index 10fa995ffda..54bef4948bf 100644 --- a/core/block_svg.ts +++ b/packages/blockly/core/block_svg.ts @@ -16,7 +16,6 @@ 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'; @@ -34,21 +33,21 @@ 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 {getFocusManager} from './focus_manager.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 {IContextMenu} from './interfaces/i_contextmenu.js'; import type {ICopyable} from './interfaces/i_copyable.js'; +import {IDeletable} from './interfaces/i_deletable.js'; import type {IDragStrategy, IDraggable} from './interfaces/i_draggable.js'; +import type {IFocusableNode} from './interfaces/i_focusable_node.js'; +import type {IFocusableTree} from './interfaces/i_focusable_tree.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'; @@ -56,8 +55,8 @@ 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 {idGenerator} from './utils.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'; @@ -73,11 +72,12 @@ import type {WorkspaceSvg} from './workspace_svg.js'; export class BlockSvg extends Block implements - IASTNodeLocationSvg, IBoundedElement, + IContextMenu, ICopyable, IDraggable, - IDeletable + IDeletable, + IFocusableNode { /** * Constant for identifying rows that are to be rendered inline. @@ -194,6 +194,9 @@ export class BlockSvg this.workspace = workspace; this.svgGroup = dom.createSvgElement(Svg.G, {}); + if (prototypeName) { + dom.addClass(this.svgGroup, prototypeName); + } /** A block style object. */ this.style = workspace.getRenderer().getConstants().getBlockStyle(null); @@ -209,6 +212,9 @@ export class BlockSvg // Expose this block's ID on its top-level SVG group. this.svgGroup.setAttribute('data-id', this.id); + // The page-wide unique ID of this Block used for focusing. + svgPath.id = idGenerator.getNextUniqueId(); + this.doInit_(); } @@ -228,7 +234,7 @@ export class BlockSvg this.applyColour(); this.pathObject.updateMovable(this.isMovable() || this.isInFlyout); const svg = this.getSvgRoot(); - if (!this.workspace.options.readOnly && svg) { + if (svg) { browserEvents.conditionalBind(svg, 'pointerdown', this, this.onMouseDown); } @@ -258,20 +264,14 @@ export class BlockSvg /** Selects this block. Highlights the block visually. */ select() { - if (this.isShadow()) { - this.getParent()?.select(); - return; - } this.addSelect(); + common.fireSelectedEvent(this); } /** Unselects this block. Unhighlights the block visually. */ unselect() { - if (this.isShadow()) { - this.getParent()?.unselect(); - return; - } this.removeSelect(); + common.fireSelectedEvent(null); } /** @@ -299,18 +299,44 @@ export class BlockSvg } const oldXY = this.getRelativeToSurfaceXY(); + const focusedNode = getFocusManager().getFocusedNode(); + const restoreFocus = this.getSvgRoot().contains( + focusedNode?.getFocusableElement() ?? null, + ); if (newParent) { (newParent as BlockSvg).getSvgRoot().appendChild(svgRoot); + // appendChild() clears focus state, so re-focus the previously focused + // node in case it was this block and would otherwise lose its focus. Once + // Element.moveBefore() has better browser support, it should be used + // instead. + if (restoreFocus && focusedNode) { + getFocusManager().focusNode(focusedNode); + } } 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 + // root of the workspace. Try to insert it before any top-level + // block being dragged, but note that blocks can have the + // blocklyDragging class even if they're not top blocks (especially + // at start and end of a drag). + const draggingBlockElement = this.workspace .getCanvas() .querySelector('.blocklyDragging'); - if (draggingBlock) { - this.workspace.getCanvas().insertBefore(svgRoot, draggingBlock); + const draggingParentElement = draggingBlockElement?.parentElement as + | SVGElement + | null + | undefined; + const canvas = this.workspace.getCanvas(); + if (draggingParentElement === canvas) { + canvas.insertBefore(svgRoot, draggingBlockElement); } else { - this.workspace.getCanvas().appendChild(svgRoot); + canvas.appendChild(svgRoot); + // appendChild() clears focus state, so re-focus the previously focused + // node in case it was this block and would otherwise lose its focus. Once + // Element.moveBefore() has better browser support, it should be used + // instead. + if (restoreFocus && focusedNode) { + getFocusManager().focusNode(focusedNode); + } } this.translate(oldXY.x, oldXY.y); } @@ -328,8 +354,8 @@ export class BlockSvg * @returns Object with .x and .y properties in workspace coordinates. */ override getRelativeToSurfaceXY(): Coordinate { - const layerManger = this.workspace.getLayerManager(); - if (!layerManger) { + const layerManager = this.workspace.getLayerManager(); + if (!layerManager) { throw new Error( 'Cannot calculate position because the workspace has not been appended', ); @@ -345,7 +371,7 @@ export class BlockSvg x += xy.x; y += xy.y; element = element.parentNode as SVGElement; - } while (element && !layerManger.hasLayer(element)); + } while (element && !layerManager.hasLayer(element)); } return new Coordinate(x, y); } @@ -507,6 +533,31 @@ export class BlockSvg this.updateCollapsed(); } + /** + * Traverses child blocks to see if any of them have a warning. + * + * @returns true if any child has a warning, false otherwise. + */ + private childHasWarning(): boolean { + const next = this.getNextBlock(); + const excluded = next ? new Set(next.getDescendants(false)) : null; + const descendants = this.getDescendants(false); + + for (const descendant of descendants) { + if (descendant === this) { + continue; + } + if (excluded?.has(descendant)) { + continue; + } + if (descendant.getIcon(WarningIcon.TYPE)) { + return true; + } + } + + return false; + } + /** * Makes sure that when the block is collapsed, it is rendered correctly * for that state. @@ -529,9 +580,19 @@ export class BlockSvg if (!collapsed) { this.updateDisabled(); this.removeInput(collapsedInputName); + dom.removeClass(this.svgGroup, 'blocklyCollapsed'); + this.setWarningText(null, BlockSvg.COLLAPSED_WARNING_ID); return; } + dom.addClass(this.svgGroup, 'blocklyCollapsed'); + if (this.childHasWarning()) { + this.setWarningText( + Msg['COLLAPSED_WARNINGS_WARNING'], + BlockSvg.COLLAPSED_WARNING_ID, + ); + } + const text = this.toString(internalConstants.COLLAPSE_CHARS); const field = this.getField(collapsedFieldName); if (field) { @@ -544,41 +605,14 @@ export class BlockSvg 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) { + if (this.workspace.isReadOnly()) return; + const gesture = this.workspace.getGesture(e); if (gesture) { gesture.handleBlockStart(e, this); @@ -603,15 +637,15 @@ export class BlockSvg * * @returns Context menu options or null if no menu. */ - protected generateContextMenu(): Array< - ContextMenuOption | LegacyContextMenuOption - > | null { - if (this.workspace.options.readOnly || !this.contextMenu) { + protected generateContextMenu( + e: Event, + ): Array | null { + if (this.workspace.isReadOnly() || !this.contextMenu) { return null; } const menuOptions = ContextMenuRegistry.registry.getContextMenuOptions( - ContextMenuRegistry.ScopeType.BLOCK, - {block: this}, + {block: this, focusedNode: this}, + e, ); // Allow the block to add or modify menuOptions. @@ -622,17 +656,57 @@ export class BlockSvg return menuOptions; } + /** + * Gets the location in which to show the context menu for this block. + * Use the location of a click if the block was clicked, or a location + * based on the block's fields otherwise. + */ + protected calculateContextMenuLocation(e: Event): Coordinate { + // Open the menu where the user clicked, if they clicked + if (e instanceof PointerEvent) { + return new Coordinate(e.clientX, e.clientY); + } + + // Otherwise, calculate a location. + // Get the location of the top-left corner of the block in + // screen coordinates. + const blockCoords = svgMath.wsToScreenCoordinates( + this.workspace, + this.getRelativeToSurfaceXY(), + ); + + // Prefer a y position below the first field in the block. + const fieldBoundingClientRect = this.inputList + .filter((input) => input.isVisible()) + .flatMap((input) => input.fieldRow) + .find((f) => f.isVisible()) + ?.getSvgRoot() + ?.getBoundingClientRect(); + + const y = + fieldBoundingClientRect && fieldBoundingClientRect.height + ? fieldBoundingClientRect.y + fieldBoundingClientRect.height + : blockCoords.y + this.height; + + return new Coordinate( + this.RTL ? blockCoords.x - 5 : blockCoords.x + 5, + y + 5, + ); + } + /** * Show the context menu for this block. * * @param e Mouse event. * @internal */ - showContextMenu(e: PointerEvent) { - const menuOptions = this.generateContextMenu(); + showContextMenu(e: Event) { + const menuOptions = this.generateContextMenu(e); + + const location = this.calculateContextMenuLocation(e); if (menuOptions && menuOptions.length) { - ContextMenu.show(e, menuOptions, this.RTL, this.workspace); + ContextMenu.show(e, menuOptions, this.RTL, this.workspace, location); ContextMenu.setCurrentBlock(this); } } @@ -676,6 +750,24 @@ export class BlockSvg } } + /** + * Add a CSS class to the SVG group of this block. + * + * @param className + */ + addClass(className: string) { + dom.addClass(this.svgGroup, className); + } + + /** + * Remove a CSS class from the SVG group of this block. + * + * @param className + */ + removeClass(className: string) { + dom.removeClass(this.svgGroup, className); + } + /** * Recursively adds or removes the dragging class to this node and its * children. @@ -688,10 +780,10 @@ export class BlockSvg if (adding) { this.translation = ''; common.draggingConnections.push(...this.getConnections_(true)); - dom.addClass(this.svgGroup, 'blocklyDragging'); + this.addClass('blocklyDragging'); } else { common.draggingConnections.length = 0; - dom.removeClass(this.svgGroup, 'blocklyDragging'); + this.removeClass('blocklyDragging'); } // Recurse through all blocks attached under this one. for (let i = 0; i < this.childBlocks_.length; i++) { @@ -716,6 +808,13 @@ export class BlockSvg */ override setEditable(editable: boolean) { super.setEditable(editable); + + if (editable) { + dom.removeClass(this.svgGroup, 'blocklyNotEditable'); + } else { + dom.addClass(this.svgGroup, 'blocklyNotEditable'); + } + const icons = this.getIcons(); for (let i = 0; i < icons.length; i++) { icons[i].updateEditable(); @@ -778,30 +877,42 @@ export class BlockSvg Tooltip.dispose(); ContextMenu.hide(); + // If this block (or a descendant) was focused, focus its parent or + // workspace instead. + const focusManager = getFocusManager(); + if ( + this.getSvgRoot().contains( + focusManager.getFocusedNode()?.getFocusableElement() ?? null, + ) + ) { + let parent: BlockSvg | undefined | null = this.getParent(); + if (!parent) { + // In some cases, blocks are disconnected from their parents before + // being deleted. Attempt to infer if there was a parent by checking + // for a connection within a radius of 0. Even if this wasn't a parent, + // it must be adjacent to this block and so is as good an option as any + // to focus after deleting. + const connection = this.outputConnection ?? this.previousConnection; + if (connection) { + const targetConnection = connection.closest( + 0, + new Coordinate(0, 0), + ).connection; + parent = targetConnection?.getSourceBlock(); + } + } + if (parent) { + focusManager.focusNode(parent); + } else { + setTimeout(() => focusManager.focusTree(this.workspace), 0); + } + } + 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); } @@ -814,8 +925,7 @@ export class BlockSvg this.disposing = true; super.disposeInternal(); - if (common.getSelected() === this) { - this.unselect(); + if (getFocusManager().getFocusedNode() === this) { this.workspace.cancelCurrentGesture(); } @@ -851,9 +961,12 @@ export class BlockSvg /** * Encode a block for copying. * + * @param addNextBlocks If true, copy subsequent blocks attached to this one + * as well. + * * @returns Copy metadata, or null if the block is an insertion marker. */ - toCopyData(): BlockCopyData | null { + toCopyData(addNextBlocks = false): BlockCopyData | null { if (this.isInsertionMarker_) { return null; } @@ -861,7 +974,8 @@ export class BlockSvg paster: BlockPaster.TYPE, blockState: blocks.save(this, { addCoordinates: true, - addNextBlocks: false, + addNextBlocks, + saveIds: false, }) as blocks.State, typeCounts: common.getBlockTypeCounts(this, true), }; @@ -873,17 +987,15 @@ export class BlockSvg * @internal */ applyColour() { - this.pathObject.applyColour(this); + 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(); - } + for (const field of this.getFields()) { + field.applyColour(); } } @@ -1014,7 +1126,9 @@ export class BlockSvg if (this.isDeadOrDying()) return; const gesture = this.workspace.getGesture(e); if (gesture) { + this.bringToFront(); gesture.setStartIcon(icon); + getFocusManager().focusNode(icon); } }; } @@ -1029,30 +1143,6 @@ export class BlockSvg 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 @@ -1075,6 +1165,20 @@ export class BlockSvg } } + /** + * Add blocklyNotDeletable class when block is not deletable + * Or remove class when block is deletable + */ + override setDeletable(deletable: boolean) { + super.setDeletable(deletable); + + if (deletable) { + dom.removeClass(this.svgGroup, 'blocklyNotDeletable'); + } else { + dom.addClass(this.svgGroup, 'blocklyNotDeletable'); + } + } + /** * Set whether the block is highlighted or not. Block highlighting is * often used to visually mark blocks currently being executed. @@ -1139,7 +1243,7 @@ export class BlockSvg .getConstants() .getBlockStyleForColour(this.colour_); - this.pathObject.setStyle(styleObj.style); + this.pathObject.setStyle?.(styleObj.style); this.style = styleObj.style; this.styleName_ = styleObj.name; @@ -1157,16 +1261,22 @@ export class BlockSvg .getRenderer() .getConstants() .getBlockStyle(blockStyleName); - this.styleName_ = blockStyleName; + + if (this.styleName_) { + dom.removeClass(this.svgGroup, this.styleName_); + } if (blockStyle) { this.hat = blockStyle.hat; - this.pathObject.setStyle(blockStyle); + this.pathObject.setStyle?.(blockStyle); // Set colour to match Block. this.colour_ = blockStyle.colourPrimary; this.style = blockStyle; this.applyColour(); + + dom.addClass(this.svgGroup, blockStyleName); + this.styleName_ = blockStyleName; } else { throw Error('Invalid style name: ' + blockStyleName); } @@ -1193,6 +1303,7 @@ export class BlockSvg * adjusting its parents. */ bringToFront(blockOnly = false) { + const previouslyFocused = getFocusManager().getFocusedNode(); /* eslint-disable-next-line @typescript-eslint/no-this-alias */ let block: this | null = this; if (block.isDeadOrDying()) { @@ -1209,6 +1320,13 @@ export class BlockSvg if (blockOnly) break; block = block.getParent(); } while (block); + if (previouslyFocused) { + // Bringing a block to the front of the stack doesn't fundamentally change + // the logical structure of the page, but it does change element ordering + // which can take automatically take away focus from a node. Ensure focus + // is restored to avoid a discontinuity. + getFocusManager().focusNode(previouslyFocused); + } } /** @@ -1571,7 +1689,6 @@ export class BlockSvg this.tightenChildrenEfficiently(); dom.stopTextWidthCache(); - this.updateMarkers_(); } /** @@ -1591,44 +1708,6 @@ export class BlockSvg 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. @@ -1681,11 +1760,26 @@ export class BlockSvg ); } + /** + * Returns the drag strategy currently in use by this block. + * + * @internal + * @returns This block's drag strategy. + */ + getDragStrategy(): IDragStrategy { + return this.dragStrategy; + } + /** Sets the drag strategy for this block. */ setDragStrategy(dragStrategy: IDragStrategy) { this.dragStrategy = dragStrategy; } + /** Returns whether this block is copyable or not. */ + isCopyable(): boolean { + return this.isOwnDeletable() && this.isOwnMovable(); + } + /** Returns whether this block is movable or not. */ override isMovable(): boolean { return this.dragStrategy.isMovable(); @@ -1736,4 +1830,44 @@ export class BlockSvg traverseJson(json as unknown as {[key: string]: unknown}); return [json]; } + + override jsonInit(json: AnyDuringMigration): void { + super.jsonInit(json); + + if (json['classes']) { + this.addClass( + Array.isArray(json['classes']) + ? json['classes'].join(' ') + : json['classes'], + ); + } + } + + /** See IFocusableNode.getFocusableElement. */ + getFocusableElement(): HTMLElement | SVGElement { + return this.pathObject.svgPath; + } + + /** See IFocusableNode.getFocusableTree. */ + getFocusableTree(): IFocusableTree { + return this.workspace; + } + + /** See IFocusableNode.onNodeFocus. */ + onNodeFocus(): void { + this.select(); + this.workspace.scrollBoundsIntoView( + this.getBoundingRectangleWithoutChildren(), + ); + } + + /** See IFocusableNode.onNodeBlur. */ + onNodeBlur(): void { + this.unselect(); + } + + /** See IFocusableNode.canBeFocused. */ + canBeFocused(): boolean { + return true; + } } diff --git a/core/blockly.ts b/packages/blockly/core/blockly.ts similarity index 89% rename from core/blockly.ts rename to packages/blockly/core/blockly.ts index 01490dbb694..99112d790fb 100644 --- a/core/blockly.ts +++ b/packages/blockly/core/blockly.ts @@ -17,6 +17,7 @@ import './events/events_var_create.js'; import {Block} from './block.js'; import * as blockAnimations from './block_animations.js'; +import {BlockFlyoutInflater} from './block_flyout_inflater.js'; import {BlockSvg} from './block_svg.js'; import {BlocklyOptions} from './blockly_options.js'; import {Blocks} from './blocks.js'; @@ -24,6 +25,7 @@ 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 {ButtonFlyoutInflater} from './button_flyout_inflater.js'; import * as clipboard from './clipboard.js'; import * as comments from './comments.js'; import * as common from './common.js'; @@ -62,6 +64,7 @@ import { FieldDropdownConfig, FieldDropdownFromJsonConfig, FieldDropdownValidator, + ImageProperties, MenuGenerator, MenuGeneratorFunction, MenuOption, @@ -99,20 +102,28 @@ import { import {Flyout} from './flyout_base.js'; import {FlyoutButton} from './flyout_button.js'; import {HorizontalFlyout} from './flyout_horizontal.js'; +import {FlyoutItem} from './flyout_item.js'; import {FlyoutMetricsManager} from './flyout_metrics_manager.js'; +import {FlyoutSeparator} from './flyout_separator.js'; import {VerticalFlyout} from './flyout_vertical.js'; +import { + FocusManager, + ReturnEphemeralFocus, + getFocusManager, +} from './focus_manager.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 {IFlyoutInflater} from './interfaces/i_flyout_inflater.js'; +import {LabelFlyoutInflater} from './label_flyout_inflater.js'; +import {SeparatorFlyoutInflater} from './separator_flyout_inflater.js'; +import {FocusableTreeTraverser} from './utils/focusable_tree_traverser.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'; @@ -132,6 +143,8 @@ import { } from './interfaces/i_draggable.js'; import {IDragger} from './interfaces/i_dragger.js'; import {IFlyout} from './interfaces/i_flyout.js'; +import {IFocusableNode} from './interfaces/i_focusable_node.js'; +import {IFocusableTree} from './interfaces/i_focusable_tree.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'; @@ -155,12 +168,15 @@ import { IVariableBackedParameterModel, isVariableBackedParameterModel, } from './interfaces/i_variable_backed_parameter_model.js'; +import {IVariableMap} from './interfaces/i_variable_map.js'; +import {IVariableModel, IVariableState} from './interfaces/i_variable_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 {LineCursor} from './keyboard_nav/line_cursor.js'; import {Marker} from './keyboard_nav/marker.js'; -import {TabNavigateCursor} from './keyboard_nav/tab_navigate_cursor.js'; +import { + KeyboardNavigationController, + keyboardNavigationController, +} from './keyboard_navigation_controller.js'; import type {LayerManager} from './layer_manager.js'; import * as layers from './layers.js'; import {MarkerManager} from './marker_manager.js'; @@ -417,10 +433,20 @@ Names.prototype.populateProcedures = function ( }; // clang-format on +export * from './flyout_navigator.js'; +export * from './interfaces/i_navigation_policy.js'; +export * from './keyboard_nav/block_navigation_policy.js'; +export * from './keyboard_nav/connection_navigation_policy.js'; +export * from './keyboard_nav/field_navigation_policy.js'; +export * from './keyboard_nav/flyout_button_navigation_policy.js'; +export * from './keyboard_nav/flyout_navigation_policy.js'; +export * from './keyboard_nav/flyout_separator_navigation_policy.js'; +export * from './keyboard_nav/workspace_navigation_policy.js'; +export * from './navigator.js'; +export * from './toast.js'; + // Re-export submodules that no longer declareLegacyNamespace. export { - ASTNode, - BasicCursor, Block, BlockSvg, BlocklyOptions, @@ -435,11 +461,11 @@ export { ContextMenuItems, ContextMenuRegistry, Css, - Cursor, DeleteArea, DragTarget, Events, Extensions, + LineCursor, Procedures, ShortcutItems, Themes, @@ -471,6 +497,8 @@ export { }; export const DropDownDiv = dropDownDiv; export { + BlockFlyoutInflater, + ButtonFlyoutInflater, CodeGenerator, Field, FieldCheckbox, @@ -504,14 +532,15 @@ export { FieldVariableValidator, Flyout, FlyoutButton, + FlyoutItem, FlyoutMetricsManager, + FlyoutSeparator, + FocusManager, + FocusableTreeTraverser, CodeGenerator as Generator, Gesture, Grid, HorizontalFlyout, - IASTNodeLocation, - IASTNodeLocationSvg, - IASTNodeLocationWithBlock, IAutoHideable, IBoundedElement, IBubble, @@ -529,6 +558,9 @@ export { IDraggable, IDragger, IFlyout, + IFlyoutInflater, + IFocusableNode, + IFocusableTree, IHasBubble, IIcon, IKeyboardAccessible, @@ -546,9 +578,14 @@ export { IToolbox, IToolboxItem, IVariableBackedParameterModel, + IVariableMap, + IVariableModel, + IVariableState, + ImageProperties, Input, - InsertionMarkerManager, InsertionMarkerPreviewer, + KeyboardNavigationController, + LabelFlyoutInflater, LayerManager, Marker, MarkerManager, @@ -562,10 +599,11 @@ export { Names, Options, RenderedConnection, + ReturnEphemeralFocus, Scrollbar, ScrollbarPair, + SeparatorFlyoutInflater, ShortcutRegistry, - TabNavigateCursor, Theme, ThemeManager, Toolbox, @@ -583,6 +621,7 @@ export { WorkspaceSvg, ZoomControls, config, + getFocusManager, hasBubble, icons, inject, @@ -597,6 +636,7 @@ export { isSelectable, isSerializable, isVariableBackedParameterModel, + keyboardNavigationController, layers, renderManagement, serialization, diff --git a/core/blockly_options.ts b/packages/blockly/core/blockly_options.ts similarity index 100% rename from core/blockly_options.ts rename to packages/blockly/core/blockly_options.ts diff --git a/core/blocks.ts b/packages/blockly/core/blocks.ts similarity index 100% rename from core/blocks.ts rename to packages/blockly/core/blocks.ts diff --git a/core/browser_events.ts b/packages/blockly/core/browser_events.ts similarity index 90% rename from core/browser_events.ts rename to packages/blockly/core/browser_events.ts index 8176fe10ff3..065a5bc5390 100644 --- a/core/browser_events.ts +++ b/packages/blockly/core/browser_events.ts @@ -46,6 +46,9 @@ const PAGE_MODE_MULTIPLIER = 125; * @param opt_noCaptureIdentifier True if triggering on this event should not * block execution of other event handlers on this touch or other * simultaneous touches. False by default. + * @param options An object with options controlling the behavior of the event + * listener. Passed through directly as the third argument to + * `addEventListener`. * @returns Opaque data that can be passed to unbindEvent_. */ export function conditionalBind( @@ -54,6 +57,7 @@ export function conditionalBind( thisObject: object | null, func: Function, opt_noCaptureIdentifier?: boolean, + options?: AddEventListenerOptions, ): Data { /** * @@ -75,11 +79,11 @@ export function conditionalBind( 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); + node.addEventListener(type, wrapFunc, {capture: false, ...options}); bindData.push([node, type, wrapFunc]); } } else { - node.addEventListener(name, wrapFunc, false); + node.addEventListener(name, wrapFunc, {capture: false, ...options}); bindData.push([node, name, wrapFunc]); } return bindData; @@ -95,6 +99,9 @@ export function conditionalBind( * @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 options An object with options controlling the behavior of the event + * listener. Passed through directly as the third argument to + * `addEventListener`. * @returns Opaque data that can be passed to unbindEvent_. */ export function bind( @@ -102,6 +109,7 @@ export function bind( name: string, thisObject: object | null, func: Function, + options?: AddEventListenerOptions, ): Data { /** * @@ -119,11 +127,11 @@ export function bind( 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); + node.addEventListener(type, wrapFunc, {capture: false, ...options}); bindData.push([node, type, wrapFunc]); } } else { - node.addEventListener(name, wrapFunc, false); + node.addEventListener(name, wrapFunc, {capture: false, ...options}); bindData.push([node, name, wrapFunc]); } return bindData; diff --git a/core/bubbles.ts b/packages/blockly/core/bubbles.ts similarity index 100% rename from core/bubbles.ts rename to packages/blockly/core/bubbles.ts diff --git a/core/bubbles/bubble.ts b/packages/blockly/core/bubbles/bubble.ts similarity index 88% rename from core/bubbles/bubble.ts rename to packages/blockly/core/bubbles/bubble.ts index bac94dbc8a0..742d300adf1 100644 --- a/core/bubbles/bubble.ts +++ b/packages/blockly/core/bubbles/bubble.ts @@ -4,11 +4,15 @@ * 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 {getFocusManager} from '../focus_manager.js'; import {IBubble} from '../interfaces/i_bubble.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; +import type {IFocusableTree} from '../interfaces/i_focusable_tree.js'; +import type {IHasBubble} from '../interfaces/i_has_bubble.js'; +import {ISelectable} from '../interfaces/i_selectable.js'; import {ContainerRegion} from '../metrics_manager.js'; import {Scrollbar} from '../scrollbar.js'; import {Coordinate} from '../utils/coordinate.js'; @@ -25,7 +29,7 @@ import {WorkspaceSvg} from '../workspace_svg.js'; * 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 { +export abstract class Bubble implements IBubble, ISelectable, IFocusableNode { /** The width of the border around the bubble. */ static readonly BORDER_WIDTH = 6; @@ -86,17 +90,26 @@ export abstract class Bubble implements IBubble, ISelectable { private dragStrategy = new BubbleDragStrategy(this, this.workspace); + private focusableElement: SVGElement | HTMLElement; + /** * @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. + * @param overriddenFocusableElement An optional replacement to the focusable + * element that's represented by this bubble (as a focusable node). This + * element will have its ID overwritten. If not provided, the focusable + * element of this node will default to the bubble's SVG root. + * @param owner The object responsible for hosting/spawning this bubble. */ constructor( public readonly workspace: WorkspaceSvg, protected anchor: Coordinate, protected ownerRect?: Rect, + overriddenFocusableElement?: SVGElement | HTMLElement, + protected owner?: IHasBubble & IFocusableNode, ) { this.id = idGenerator.getNextUniqueId(); this.svgRoot = dom.createSvgElement( @@ -106,11 +119,7 @@ export abstract class Bubble implements IBubble, ISelectable { ); const embossGroup = dom.createSvgElement( Svg.G, - { - 'filter': `url(#${ - this.workspace.getRenderer().getConstants().embossFilterId - })`, - }, + {'class': 'blocklyEmboss'}, this.svgRoot, ); this.tail = dom.createSvgElement( @@ -131,12 +140,22 @@ export abstract class Bubble implements IBubble, ISelectable { ); this.contentContainer = dom.createSvgElement(Svg.G, {}, this.svgRoot); + this.focusableElement = overriddenFocusableElement ?? this.svgRoot; + this.focusableElement.setAttribute('id', this.id); + browserEvents.conditionalBind( this.background, 'pointerdown', this, this.onMouseDown, ); + + browserEvents.conditionalBind( + this.focusableElement, + 'keydown', + this, + this.onKeyDown, + ); } /** Dispose of this bubble. */ @@ -212,11 +231,26 @@ export abstract class Bubble implements IBubble, ISelectable { this.background.setAttribute('fill', colour); } - /** Brings the bubble to the front and passes the pointer event off to the gesture system. */ + /** + * Passes the pointer event off to the gesture system and ensures the bubble + * is focused. + */ private onMouseDown(e: PointerEvent) { this.workspace.getGesture(e)?.handleBubbleStart(e, this); - this.bringToFront(); - common.setSelected(this); + getFocusManager().focusNode(this); + } + + /** + * Handles key events when this bubble is focused. By default, closes the + * bubble on Escape. + * + * @param e The keyboard event to handle. + */ + protected onKeyDown(e: KeyboardEvent) { + if (e.key === 'Escape' && this.owner) { + this.owner.setBubbleVisible(false); + getFocusManager().focusNode(this.owner); + } } /** Positions the bubble relative to its anchor. Does not render its tail. */ @@ -651,9 +685,48 @@ export abstract class Bubble implements IBubble, ISelectable { select(): void { // Bubbles don't have any visual for being selected. + common.fireSelectedEvent(this); } unselect(): void { // Bubbles don't have any visual for being selected. + common.fireSelectedEvent(null); + } + + /** See IFocusableNode.getFocusableElement. */ + getFocusableElement(): HTMLElement | SVGElement { + return this.focusableElement; + } + + /** See IFocusableNode.getFocusableTree. */ + getFocusableTree(): IFocusableTree { + return this.workspace; + } + + /** See IFocusableNode.onNodeFocus. */ + onNodeFocus(): void { + this.select(); + this.bringToFront(); + const xy = this.getRelativeToSurfaceXY(); + const size = this.getSize(); + const bounds = new Rect(xy.y, xy.y + size.height, xy.x, xy.x + size.width); + this.workspace.scrollBoundsIntoView(bounds); + } + + /** See IFocusableNode.onNodeBlur. */ + onNodeBlur(): void { + this.unselect(); + } + + /** See IFocusableNode.canBeFocused. */ + canBeFocused(): boolean { + return true; + } + + /** + * Returns the object that owns/hosts this bubble, if any. + */ + getOwner(): (IHasBubble & IFocusableNode) | undefined { + return this.owner; } } diff --git a/core/bubbles/mini_workspace_bubble.ts b/packages/blockly/core/bubbles/mini_workspace_bubble.ts similarity index 94% rename from core/bubbles/mini_workspace_bubble.ts rename to packages/blockly/core/bubbles/mini_workspace_bubble.ts index f4ad96c8c00..194cb41f35d 100644 --- a/core/bubbles/mini_workspace_bubble.ts +++ b/packages/blockly/core/bubbles/mini_workspace_bubble.ts @@ -80,6 +80,7 @@ export class MiniWorkspaceBubble extends Bubble { flyout?.show(options.languageTree); } + dom.addClass(this.svgRoot, 'blocklyMiniWorkspaceBubble'); this.miniWorkspace.addChangeListener(this.onWorkspaceChange.bind(this)); this.miniWorkspace .getFlyout() @@ -152,7 +153,11 @@ export class MiniWorkspaceBubble extends Bubble { * are dealt with by resizing the workspace to show them. */ private bumpBlocksIntoBounds() { - if (this.miniWorkspace.isDragging()) return; + if ( + this.miniWorkspace.isDragging() && + !this.miniWorkspace.keyboardMoveInProgress + ) + return; const MARGIN = 20; @@ -184,7 +189,15 @@ export class MiniWorkspaceBubble extends Bubble { * mini workspace. */ private updateBubbleSize() { - if (this.miniWorkspace.isDragging()) return; + if ( + this.miniWorkspace.isDragging() && + !this.miniWorkspace.keyboardMoveInProgress + ) + return; + + // Disable autolayout if a keyboard move is in progress to prevent the + // mutator bubble from jumping around. + this.autoLayout &&= !this.miniWorkspace.keyboardMoveInProgress; const currSize = this.getSize(); const newSize = this.calculateWorkspaceSize(); diff --git a/core/bubbles/text_bubble.ts b/packages/blockly/core/bubbles/text_bubble.ts similarity index 98% rename from core/bubbles/text_bubble.ts rename to packages/blockly/core/bubbles/text_bubble.ts index 6db81cd99bc..99299fa50e8 100644 --- a/core/bubbles/text_bubble.ts +++ b/packages/blockly/core/bubbles/text_bubble.ts @@ -27,6 +27,7 @@ export class TextBubble extends Bubble { super(workspace, anchor, ownerRect); this.paragraph = this.stringToSvg(text, this.contentContainer); this.updateBubbleSize(); + dom.addClass(this.svgRoot, 'blocklyTextBubble'); } /** @returns the current text of this text bubble. */ diff --git a/core/bubbles/textinput_bubble.ts b/packages/blockly/core/bubbles/textinput_bubble.ts similarity index 70% rename from core/bubbles/textinput_bubble.ts rename to packages/blockly/core/bubbles/textinput_bubble.ts index 5b5278b91ff..0bad5fabce6 100644 --- a/core/bubbles/textinput_bubble.ts +++ b/packages/blockly/core/bubbles/textinput_bubble.ts @@ -4,7 +4,11 @@ * SPDX-License-Identifier: Apache-2.0 */ +import {CommentEditor} from '../comments/comment_editor.js'; import * as Css from '../css.js'; +import {getFocusManager} from '../focus_manager.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; +import type {IHasBubble} from '../interfaces/i_has_bubble.js'; import * as touch from '../touch.js'; import {browserEvents} from '../utils.js'; import {Coordinate} from '../utils/coordinate.js'; @@ -21,12 +25,6 @@ import {Bubble} from './bubble.js'; * 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; @@ -42,14 +40,11 @@ export class TextInputBubble extends Bubble { */ 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 = ''; + /** Functions listening for changes to the location of this bubble. */ + private locationChangeListeners: (() => void)[] = []; /** The default size of this bubble, including borders. */ private readonly DEFAULT_SIZE = new Size( @@ -65,47 +60,47 @@ export class TextInputBubble extends Bubble { private editable = true; + /** View responsible for supporting text editing. */ + private editor: CommentEditor; + /** * @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. + * @param owner The object that owns/hosts this bubble. */ constructor( public readonly workspace: WorkspaceSvg, protected anchor: Coordinate, protected ownerRect?: Rect, + protected owner?: IHasBubble & IFocusableNode, ) { - super(workspace, anchor, ownerRect); + super(workspace, anchor, ownerRect, undefined, owner); dom.addClass(this.svgRoot, 'blocklyTextInputBubble'); - ({inputRoot: this.inputRoot, textArea: this.textArea} = this.createEditor( - this.contentContainer, - )); + this.editor = new CommentEditor(workspace, this.id, () => { + getFocusManager().focusNode(this); + }); + this.contentContainer.appendChild(this.editor.getDom()); this.resizeGroup = this.createResizeHandle(this.svgRoot, workspace); this.setSize(this.DEFAULT_SIZE, true); } /** @returns the text of this bubble. */ getText(): string { - return this.text; + return this.editor.getText(); } /** Sets the text of this bubble. Calls change listeners. */ setText(text: string) { - this.text = text; - this.textArea.value = text; - this.onTextChange(); + this.editor.setText(text); } /** 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', ''); - } + this.editor.setEditable(editable); } /** Returns whether or not the text in the bubble is editable. */ @@ -115,7 +110,7 @@ export class TextInputBubble extends Bubble { /** Adds a change listener to be notified when this bubble's text changes. */ addTextChangeListener(listener: () => void) { - this.textChangeListeners.push(listener); + this.editor.addTextChangeListener(listener); } /** Adds a change listener to be notified when this bubble's size changes. */ @@ -123,57 +118,9 @@ export class TextInputBubble extends Bubble { 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); + /** Adds a change listener to be notified when this bubble's location changes. */ + addLocationChangeListener(listener: () => void) { + this.locationChangeListeners.push(listener); } /** Creates the resize handler elements and binds events to them. */ @@ -214,8 +161,12 @@ export class TextInputBubble extends Bubble { 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.editor.updateSize( + new Size(widthMinusBorder, heightMinusBorder), + new Size(0, 0), + ); + this.editor.getDom().setAttribute('x', `${Bubble.DOUBLE_BORDER / 2}`); + this.editor.getDom().setAttribute('y', `${Bubble.DOUBLE_BORDER / 2}`); this.resizeGroup.setAttribute('y', `${heightMinusBorder}`); if (this.workspace.RTL) { @@ -230,10 +181,25 @@ export class TextInputBubble extends Bubble { /** @returns the size of this bubble. */ getSize(): Size { - // Overriden to be public. + // Overridden to be public. return super.getSize(); } + override moveDuringDrag(newLoc: Coordinate) { + super.moveDuringDrag(newLoc); + this.onLocationChange(); + } + + override setPositionRelativeToAnchor(left: number, top: number) { + super.setPositionRelativeToAnchor(left, top); + this.onLocationChange(); + } + + protected override positionByRect(rect = new Rect(0, 0, 0, 0)) { + super.positionByRect(rect); + this.onLocationChange(); + } + /** Handles mouse down events on the resize target. */ private onResizePointerDown(e: PointerEvent) { this.bringToFront(); @@ -291,30 +257,27 @@ export class TextInputBubble extends Bubble { 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 size change event for the text area. Calls event listeners. */ + private onSizeChange() { + for (const listener of this.sizeChangeListeners) { + listener(); } } - /** 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) { + /** Handles a location change event for the text area. Calls event listeners. */ + private onLocationChange() { + for (const listener of this.locationChangeListeners) { listener(); } } - /** Handles a size change event for the text area. Calls event listeners. */ - private onSizeChange() { - for (const listener of this.sizeChangeListeners) { - listener(); - } + /** + * Returns the text editor component of this bubble. + * + * @internal + */ + getEditor() { + return this.editor; } } diff --git a/core/bump_objects.ts b/packages/blockly/core/bump_objects.ts similarity index 100% rename from core/bump_objects.ts rename to packages/blockly/core/bump_objects.ts diff --git a/packages/blockly/core/button_flyout_inflater.ts b/packages/blockly/core/button_flyout_inflater.ts new file mode 100644 index 00000000000..4f083f015f7 --- /dev/null +++ b/packages/blockly/core/button_flyout_inflater.ts @@ -0,0 +1,76 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {FlyoutButton} from './flyout_button.js'; +import {FlyoutItem} from './flyout_item.js'; +import type {IFlyout} from './interfaces/i_flyout.js'; +import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js'; +import * as registry from './registry.js'; +import {ButtonOrLabelInfo} from './utils/toolbox.js'; + +const BUTTON_TYPE = 'button'; + +/** + * Class responsible for creating buttons for flyouts. + */ +export class ButtonFlyoutInflater implements IFlyoutInflater { + /** + * Inflates a flyout button from the given state and adds it to the flyout. + * + * @param state A JSON representation of a flyout button. + * @param flyout The flyout to create the button on. + * @returns A newly created FlyoutButton. + */ + load(state: object, flyout: IFlyout): FlyoutItem { + const button = new FlyoutButton( + flyout.getWorkspace(), + flyout.targetWorkspace!, + state as ButtonOrLabelInfo, + false, + ); + button.show(); + + return new FlyoutItem(button, BUTTON_TYPE); + } + + /** + * Returns the amount of space that should follow this button. + * + * @param state A JSON representation of a flyout button. + * @param defaultGap The default spacing for flyout items. + * @returns The amount of space that should follow this button. + */ + gapForItem(state: object, defaultGap: number): number { + return defaultGap; + } + + /** + * Disposes of the given button. + * + * @param item The flyout button to dispose of. + */ + disposeItem(item: FlyoutItem): void { + const element = item.getElement(); + if (element instanceof FlyoutButton) { + element.dispose(); + } + } + + /** + * Returns the type of items this inflater is responsible for creating. + * + * @returns An identifier for the type of items this inflater creates. + */ + getType() { + return BUTTON_TYPE; + } +} + +registry.register( + registry.Type.FLYOUT_INFLATER, + BUTTON_TYPE, + ButtonFlyoutInflater, +); diff --git a/packages/blockly/core/clipboard.ts b/packages/blockly/core/clipboard.ts new file mode 100644 index 00000000000..c7b22dfc7a8 --- /dev/null +++ b/packages/blockly/core/clipboard.ts @@ -0,0 +1,197 @@ +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.clipboard + +import {BlockCopyData, BlockPaster} from './clipboard/block_paster.js'; +import * as registry from './clipboard/registry.js'; +import type {ICopyData, ICopyable} from './interfaces/i_copyable.js'; +import {isSelectable} from './interfaces/i_selectable.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; + +let stashedCoordinates: Coordinate | undefined = undefined; + +/** + * Copy a copyable item, and record its data and the workspace it was + * copied from. + * + * This function does not perform any checks to ensure the copy + * should be allowed, e.g. to ensure the block is deletable. Such + * checks should be done before calling this function. + * + * Note that if the copyable item is not an `ISelectable` or its + * `workspace` property is not a `WorkspaceSvg`, the copy will be + * successful, but there will be no saved workspace data. This will + * impact the ability to paste the data unless you explictily pass + * a workspace into the paste method. + * + * @param toCopy item to copy. + * @param location location to save as a potential paste location. + * @returns the copied data if copy was successful, otherwise null. + */ +export function copy( + toCopy: ICopyable, + location?: Coordinate, +): T | null { + const data = toCopy.toCopyData(); + stashedCopyData = data; + if (isSelectable(toCopy) && toCopy.workspace instanceof WorkspaceSvg) { + stashedWorkspace = toCopy.workspace; + } else { + stashedWorkspace = null; + } + + stashedCoordinates = location; + return data; +} + +/** + * Gets the copy data for the last item copied. This is useful if you + * are implementing custom copy/paste behavior. If you want the default + * behavior, just use the copy and paste methods directly. + * + * @returns copy data for the last item copied, or null if none set. + */ +export function getLastCopiedData() { + return stashedCopyData; +} + +/** + * Sets the last copied item. You should call this method if you implement + * custom copy behavior, so that other callers are working with the correct + * data. This method is called automatically if you use the built-in copy + * method. + * + * @param copyData copy data for the last item copied. + */ +export function setLastCopiedData(copyData: ICopyData) { + stashedCopyData = copyData; +} + +/** + * Gets the workspace that was last copied from. This is useful if you + * are implementing custom copy/paste behavior and want to paste on the + * same workspace that was copied from. If you want the default behavior, + * just use the copy and paste methods directly. + * + * @returns workspace that was last copied from, or null if none set. + */ +export function getLastCopiedWorkspace() { + return stashedWorkspace; +} + +/** + * Sets the workspace that was last copied from. You should call this method + * if you implement custom copy behavior, so that other callers are working + * with the correct data. This method is called automatically if you use the + * built-in copy method. + * + * @param workspace workspace that was last copied from. + */ +export function setLastCopiedWorkspace(workspace: WorkspaceSvg) { + stashedWorkspace = workspace; +} + +/** + * Gets the location that was last copied from. This is useful if you + * are implementing custom copy/paste behavior. If you want the + * default behavior, just use the copy and paste methods directly. + * + * @returns last saved location, or null if none set. + */ +export function getLastCopiedLocation() { + return stashedCoordinates; +} + +/** + * Sets the location that was last copied from. You should call this method + * if you implement custom copy behavior, so that other callers are working + * with the correct data. This method is called automatically if you use the + * built-in copy method. + * + * @param location last saved location, which can be used to paste at. + */ +export function setLastCopiedLocation(location: Coordinate) { + stashedCoordinates = location; +} + +/** + * Paste a pasteable element into the given workspace. + * + * This function does not perform any checks to ensure the paste + * is allowed, e.g. that the workspace is rendered or the block + * is pasteable. Such checks should be done before calling this + * function. + * + * @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 last copied-from 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, stashedCoordinates); + } + 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.isMutator + ? workspace + : // Use the parent workspace if it exists (e.g. for pasting into flyouts) + (workspace.options.parentWorkspace ?? workspace); + return (globalRegistry + .getObject(globalRegistry.Type.PASTER, copyData.paster, false) + ?.paste(copyData, workspace, coordinate) ?? null) as ICopyable | null; +} + +export {BlockCopyData, BlockPaster, registry}; diff --git a/core/clipboard/block_paster.ts b/packages/blockly/core/clipboard/block_paster.ts similarity index 87% rename from core/clipboard/block_paster.ts rename to packages/blockly/core/clipboard/block_paster.ts index 08ff220ee91..e782cc0b004 100644 --- a/core/clipboard/block_paster.ts +++ b/packages/blockly/core/clipboard/block_paster.ts @@ -5,12 +5,14 @@ */ import {BlockSvg} from '../block_svg.js'; -import * as common from '../common.js'; +import {IFocusableNode} from '../blockly.js'; import {config} from '../config.js'; import {EventType} from '../events/type.js'; import * as eventUtils from '../events/utils.js'; +import {getFocusManager} from '../focus_manager.js'; import {ICopyData} from '../interfaces/i_copyable.js'; import {IPaster} from '../interfaces/i_paster.js'; +import * as renderManagement from '../render_management.js'; import {State, append} from '../serialization/blocks.js'; import {Coordinate} from '../utils/coordinate.js'; import {WorkspaceSvg} from '../workspace_svg.js'; @@ -55,7 +57,13 @@ export class BlockPaster implements IPaster { if (eventUtils.isEnabled() && !block.isShadow()) { eventUtils.fire(new (eventUtils.get(EventType.BLOCK_CREATE))(block)); } - common.setSelected(block); + + // Sometimes there's a delay before the block is fully created and ready for + // focusing, so wait slightly before focusing the newly pasted block. + const nodeToFocus: IFocusableNode = block; + renderManagement + .finishQueuedRenders() + .then(() => getFocusManager().focusNode(nodeToFocus)); return block; } } @@ -75,6 +83,9 @@ export function moveBlockToNotConflict( block: BlockSvg, originalPosition: Coordinate, ) { + if (block.workspace.RTL) { + originalPosition.x = block.workspace.getWidth() - originalPosition.x; + } const workspace = block.workspace; const snapRadius = config.snapRadius; const bumpOffset = Coordinate.difference( diff --git a/core/clipboard/registry.ts b/packages/blockly/core/clipboard/registry.ts similarity index 100% rename from core/clipboard/registry.ts rename to packages/blockly/core/clipboard/registry.ts diff --git a/core/clipboard/workspace_comment_paster.ts b/packages/blockly/core/clipboard/workspace_comment_paster.ts similarity index 96% rename from core/clipboard/workspace_comment_paster.ts rename to packages/blockly/core/clipboard/workspace_comment_paster.ts index fdfbf0a8419..00c56681dd3 100644 --- a/core/clipboard/workspace_comment_paster.ts +++ b/packages/blockly/core/clipboard/workspace_comment_paster.ts @@ -5,9 +5,9 @@ */ 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 {getFocusManager} from '../focus_manager.js'; import {ICopyData} from '../interfaces/i_copyable.js'; import {IPaster} from '../interfaces/i_paster.js'; import * as commentSerialiation from '../serialization/workspace_comments.js'; @@ -49,7 +49,7 @@ export class WorkspaceCommentPaster if (eventUtils.isEnabled()) { eventUtils.fire(new (eventUtils.get(EventType.COMMENT_CREATE))(comment)); } - common.setSelected(comment); + getFocusManager().focusNode(comment); return comment; } } diff --git a/core/comments.ts b/packages/blockly/core/comments.ts similarity index 50% rename from core/comments.ts rename to packages/blockly/core/comments.ts index ee85919873a..179ab4a33d0 100644 --- a/core/comments.ts +++ b/packages/blockly/core/comments.ts @@ -4,6 +4,10 @@ * SPDX-License-Identifier: Apache-2.0 */ +export {CollapseCommentBarButton} from './comments/collapse_comment_bar_button.js'; +export {CommentBarButton} from './comments/comment_bar_button.js'; +export {CommentEditor} from './comments/comment_editor.js'; export {CommentView} from './comments/comment_view.js'; +export {DeleteCommentBarButton} from './comments/delete_comment_bar_button.js'; export {RenderedWorkspaceComment} from './comments/rendered_workspace_comment.js'; export {WorkspaceComment} from './comments/workspace_comment.js'; diff --git a/packages/blockly/core/comments/collapse_comment_bar_button.ts b/packages/blockly/core/comments/collapse_comment_bar_button.ts new file mode 100644 index 00000000000..304e2af8125 --- /dev/null +++ b/packages/blockly/core/comments/collapse_comment_bar_button.ts @@ -0,0 +1,102 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as browserEvents from '../browser_events.js'; +import * as touch from '../touch.js'; +import * as dom from '../utils/dom.js'; +import {Svg} from '../utils/svg.js'; +import type {WorkspaceSvg} from '../workspace_svg.js'; +import {CommentBarButton} from './comment_bar_button.js'; +import type {CommentView} from './comment_view.js'; + +/** + * Magic string appended to the comment ID to create a unique ID for this button. + */ +export const COMMENT_COLLAPSE_BAR_BUTTON_FOCUS_IDENTIFIER = + '_collapse_bar_button'; + +/** + * Button that toggles the collapsed state of a comment. + */ +export class CollapseCommentBarButton extends CommentBarButton { + /** + * Opaque ID used to unbind event handlers during disposal. + */ + private readonly bindId: browserEvents.Data; + + /** + * SVG image displayed on this button. + */ + protected override readonly icon: SVGImageElement; + + /** + * Creates a new CollapseCommentBarButton instance. + * + * @param id The ID of this button's parent comment. + * @param workspace The workspace this button's parent comment is displayed on. + * @param container An SVG group that this button should be a child of. + */ + constructor( + protected readonly id: string, + protected readonly workspace: WorkspaceSvg, + protected readonly container: SVGGElement, + protected readonly commentView: CommentView, + ) { + super(id, workspace, container, commentView); + + this.icon = dom.createSvgElement( + Svg.IMAGE, + { + 'class': 'blocklyFoldoutIcon', + 'href': `${this.workspace.options.pathToMedia}foldout-icon.svg`, + 'id': `${this.id}${COMMENT_COLLAPSE_BAR_BUTTON_FOCUS_IDENTIFIER}`, + }, + this.container, + ); + this.bindId = browserEvents.conditionalBind( + this.icon, + 'pointerdown', + this, + this.performAction.bind(this), + ); + } + + /** + * Disposes of this button. + */ + dispose() { + browserEvents.unbind(this.bindId); + } + + /** + * Adjusts the positioning of this button within its container. + */ + override reposition() { + const margin = this.getMargin(); + this.icon.setAttribute('y', `${margin}`); + this.icon.setAttribute('x', `${margin}`); + } + + /** + * Toggles the collapsed state of the parent comment. + * + * @param e The event that triggered this action. + */ + override performAction(e?: Event) { + touch.clearTouchIdentifier(); + + this.getCommentView().bringToFront(); + if (e && e instanceof PointerEvent && browserEvents.isRightButton(e)) { + e.stopPropagation(); + return; + } + + this.getCommentView().setCollapsed(!this.getCommentView().isCollapsed()); + this.workspace.hideChaff(); + + e?.stopPropagation(); + } +} diff --git a/packages/blockly/core/comments/comment_bar_button.ts b/packages/blockly/core/comments/comment_bar_button.ts new file mode 100644 index 00000000000..be130b0e335 --- /dev/null +++ b/packages/blockly/core/comments/comment_bar_button.ts @@ -0,0 +1,105 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; +import {Rect} from '../utils/rect.js'; +import type {WorkspaceSvg} from '../workspace_svg.js'; +import type {CommentView} from './comment_view.js'; + +/** + * Button displayed on a comment's top bar. + */ +export abstract class CommentBarButton implements IFocusableNode { + /** + * SVG image displayed on this button. + */ + protected abstract readonly icon: SVGImageElement; + + /** + * Creates a new CommentBarButton instance. + * + * @param id The ID of this button's parent comment. + * @param workspace The workspace this button's parent comment is on. + * @param container An SVG group that this button should be a child of. + */ + constructor( + protected readonly id: string, + protected readonly workspace: WorkspaceSvg, + protected readonly container: SVGGElement, + protected readonly commentView: CommentView, + ) {} + + /** + * Returns whether or not this button is currently visible. + */ + isVisible(): boolean { + return this.icon.checkVisibility(); + } + + /** + * Returns the parent comment view of this comment bar button. + */ + getCommentView(): CommentView { + return this.commentView; + } + + /** Adjusts the position of this button within its parent container. */ + abstract reposition(): void; + + /** Perform the action this button should take when it is acted on. */ + abstract performAction(e?: Event): void; + + /** + * Returns the dimensions of this button in workspace coordinates. + * + * @param includeMargin True to include the margin when calculating the size. + * @returns The size of this button. + */ + getSize(includeMargin = false): Rect { + const bounds = this.icon.getBBox(); + const rect = Rect.from(bounds); + if (includeMargin) { + const margin = this.getMargin(); + rect.left -= margin; + rect.top -= margin; + rect.bottom += margin; + rect.right += margin; + } + return rect; + } + + /** Returns the margin in workspace coordinates surrounding this button. */ + getMargin(): number { + return (this.container.getBBox().height - this.icon.getBBox().height) / 2; + } + + /** Returns a DOM element representing this button that can receive focus. */ + getFocusableElement() { + return this.icon; + } + + /** Returns the workspace this button is a child of. */ + getFocusableTree() { + return this.workspace; + } + + /** Called when this button's focusable DOM element gains focus. */ + onNodeFocus() { + const commentView = this.getCommentView(); + const xy = commentView.getRelativeToSurfaceXY(); + const size = commentView.getSize(); + const bounds = new Rect(xy.y, xy.y + size.height, xy.x, xy.x + size.width); + commentView.workspace.scrollBoundsIntoView(bounds); + } + + /** Called when this button's focusable DOM element loses focus. */ + onNodeBlur() {} + + /** Returns whether this button can be focused. True if it is visible. */ + canBeFocused() { + return this.isVisible(); + } +} diff --git a/packages/blockly/core/comments/comment_editor.ts b/packages/blockly/core/comments/comment_editor.ts new file mode 100644 index 00000000000..92c92fa5464 --- /dev/null +++ b/packages/blockly/core/comments/comment_editor.ts @@ -0,0 +1,220 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as browserEvents from '../browser_events.js'; +import {getFocusManager} from '../focus_manager.js'; +import {IFocusableNode} from '../interfaces/i_focusable_node.js'; +import {IFocusableTree} from '../interfaces/i_focusable_tree.js'; +import {Msg} from '../msg.js'; +import * as touch from '../touch.js'; +import * as dom from '../utils/dom.js'; +import {Rect} from '../utils/rect.js'; +import {Size} from '../utils/size.js'; +import {Svg} from '../utils/svg.js'; +import * as svgMath from '../utils/svg_math.js'; +import {WorkspaceSvg} from '../workspace_svg.js'; + +/** + * String added to the ID of a workspace comment to identify + * the focusable node for the comment editor. + */ +export const COMMENT_EDITOR_FOCUS_IDENTIFIER = '_comment_textarea_'; + +/** The part of a comment that can be typed into. */ +export class CommentEditor implements IFocusableNode { + id?: string; + /** The foreignObject containing the HTML text area. */ + private foreignObject: SVGForeignObjectElement; + + /** The text area where the user can type. */ + private textArea: HTMLTextAreaElement; + + /** Listeners for changes to text. */ + private textChangeListeners: Array< + (oldText: string, newText: string) => void + > = []; + + /** The current text of the comment. Updates on text area change. */ + private text: string = ''; + + constructor( + public workspace: WorkspaceSvg, + commentId?: string, + private onFinishEditing?: () => void, + ) { + this.foreignObject = dom.createSvgElement(Svg.FOREIGNOBJECT, { + 'class': 'blocklyCommentForeignObject', + }); + const body = document.createElementNS(dom.HTML_NS, 'body'); + body.setAttribute('xmlns', dom.HTML_NS); + body.className = 'blocklyMinimalBody'; + this.textArea = document.createElementNS( + dom.HTML_NS, + 'textarea', + ) as HTMLTextAreaElement; + this.textArea.setAttribute('tabindex', '-1'); + this.textArea.setAttribute('dir', this.workspace.RTL ? 'RTL' : 'LTR'); + this.textArea.setAttribute( + 'placeholder', + Msg['WORKSPACE_COMMENT_DEFAULT_TEXT'], + ); + dom.addClass(this.textArea, 'blocklyCommentText'); + dom.addClass(this.textArea, 'blocklyTextarea'); + dom.addClass(this.textArea, 'blocklyText'); + body.appendChild(this.textArea); + this.foreignObject.appendChild(body); + + if (commentId) { + this.id = commentId + COMMENT_EDITOR_FOCUS_IDENTIFIER; + this.textArea.setAttribute('id', this.id); + } + + // Register browser event listeners for the user typing in the textarea. + browserEvents.conditionalBind( + this.textArea, + 'change', + this, + this.onTextChange, + ); + + // Register listener for pointerdown to focus the textarea. + browserEvents.conditionalBind( + this.textArea, + 'pointerdown', + this, + (e: PointerEvent) => { + // don't allow this event to bubble up + // and steal focus away from the editor/comment. + e.stopPropagation(); + getFocusManager().focusNode(this); + touch.clearTouchIdentifier(); + }, + ); + + // Don't zoom with mousewheel; let it scroll instead. + browserEvents.conditionalBind( + this.textArea, + 'wheel', + this, + (e: Event) => { + e.stopPropagation(); + }, + false, + {passive: true}, + ); + + // Register listener for keydown events that would finish editing. + browserEvents.conditionalBind( + this.textArea, + 'keydown', + this, + this.handleKeyDown, + ); + } + + /** Gets the dom structure for this comment editor. */ + getDom(): SVGForeignObjectElement { + return this.foreignObject; + } + + /** Gets the current text of the comment. */ + getText(): string { + return this.text; + } + + /** Sets the current text of the comment and fires change listeners. */ + setText(text: string) { + this.textArea.value = text; + this.onTextChange(); + } + + /** + * 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; + // 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); + } + } + + /** + * Do something when the user indicates they've finished editing. + * + * @param e Keyboard event. + */ + private handleKeyDown(e: KeyboardEvent) { + if (e.key === 'Escape' || (e.key === 'Enter' && (e.ctrlKey || e.metaKey))) { + if (this.onFinishEditing) this.onFinishEditing(); + e.stopPropagation(); + } + } + + /** 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, + ); + } + + /** Sets the placeholder text displayed for an empty comment. */ + setPlaceholderText(text: string) { + this.textArea.placeholder = text; + } + + /** Sets whether the textarea is editable. If not, the textarea will be readonly. */ + setEditable(isEditable: boolean) { + if (isEditable) { + this.textArea.removeAttribute('readonly'); + } else { + this.textArea.setAttribute('readonly', 'true'); + } + } + + /** Update the size of the comment editor element. */ + updateSize(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}`); + } + } + + getFocusableElement(): HTMLElement | SVGElement { + return this.textArea; + } + getFocusableTree(): IFocusableTree { + return this.workspace; + } + onNodeFocus(): void { + const bbox = Rect.from(this.foreignObject.getBoundingClientRect()); + this.workspace.scrollBoundsIntoView( + Rect.createFromPoint( + svgMath.screenToWsCoordinates(this.workspace, bbox.getOrigin()), + bbox.getWidth(), + bbox.getHeight(), + ), + ); + } + onNodeBlur(): void {} + canBeFocused(): boolean { + if (this.id) return true; + return false; + } +} diff --git a/core/comments/comment_view.ts b/packages/blockly/core/comments/comment_view.ts similarity index 70% rename from core/comments/comment_view.ts rename to packages/blockly/core/comments/comment_view.ts index 99c14aaa8f2..b1cd628f8dd 100644 --- a/core/comments/comment_view.ts +++ b/packages/blockly/core/comments/comment_view.ts @@ -6,6 +6,7 @@ import * as browserEvents from '../browser_events.js'; import * as css from '../css.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node'; import {IRenderedElement} from '../interfaces/i_rendered_element.js'; import * as layers from '../layers.js'; import * as touch from '../touch.js'; @@ -15,13 +16,17 @@ 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'; +import {CollapseCommentBarButton} from './collapse_comment_bar_button.js'; +import {CommentBarButton} from './comment_bar_button.js'; +import {CommentEditor} from './comment_editor.js'; +import {DeleteCommentBarButton} from './delete_comment_bar_button.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. + * The SVG rect element that we use to create a highlight around the comment. */ private highlightRect: SVGRectElement; @@ -31,11 +36,11 @@ export class CommentView implements IRenderedElement { /** The rect background for the top bar. */ private topBarBackground: SVGRectElement; - /** The delete icon that goes in the top bar. */ - private deleteIcon: SVGImageElement; + /** The delete button that goes in the top bar. */ + private deleteButton: DeleteCommentBarButton; - /** The foldout icon that goes in the top bar. */ - private foldoutIcon: SVGImageElement; + /** The foldout button that goes in the top bar. */ + private foldoutButton: CollapseCommentBarButton; /** The text element that goes in the top bar. */ private textPreview: SVGTextElement; @@ -46,14 +51,11 @@ export class CommentView implements IRenderedElement { /** 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 part of the comment view that contains the textarea to edit the comment. */ + private commentEditor: CommentEditor; /** The current size of the comment in workspace units. */ - private size: Size = new Size(120, 100); + private size: Size; /** Whether the comment is collapsed or not. */ private collapsed: boolean = false; @@ -64,14 +66,6 @@ export class CommentView implements IRenderedElement { /** 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> = []; @@ -95,15 +89,21 @@ export class CommentView implements IRenderedElement { private resizePointerMoveListener: browserEvents.Data | null = null; /** Whether this comment view is currently being disposed or not. */ - private disposing = false; + protected disposing = false; /** Whether this comment view has been disposed or not. */ - private disposed = false; + protected disposed = false; /** Size of this comment when the resize drag was initiated. */ private preResizeSize?: Size; - constructor(private readonly workspace: WorkspaceSvg) { + /** The default size of newly created comments. */ + static defaultCommentSize = new Size(120, 100); + + constructor( + readonly workspace: WorkspaceSvg, + readonly commentId: string, + ) { this.svgRoot = dom.createSvgElement(Svg.G, { 'class': 'blocklyComment blocklyEditable blocklyDraggable', }); @@ -113,14 +113,13 @@ export class CommentView implements IRenderedElement { ({ topBarGroup: this.topBarGroup, topBarBackground: this.topBarBackground, - deleteIcon: this.deleteIcon, - foldoutIcon: this.foldoutIcon, + deleteButton: this.deleteButton, + foldoutButton: this.foldoutButton, textPreview: this.textPreview, textPreviewNode: this.textPreviewNode, - } = this.createTopBar(this.svgRoot, workspace)); + } = this.createTopBar(this.svgRoot)); - ({foreignObject: this.foreignObject, textArea: this.textArea} = - this.createTextArea(this.svgRoot)); + this.commentEditor = this.createTextArea(); this.resizeHandle = this.createResizeHandle(this.svgRoot, workspace); @@ -129,6 +128,7 @@ export class CommentView implements IRenderedElement { workspace.getLayerManager()?.append(this, layers.BLOCK); // Set size to the default size. + this.size = CommentView.defaultCommentSize; this.setSizeWithoutFiringEvents(this.size); // Set default transform (including inverted scale for RTL). @@ -150,14 +150,11 @@ export class CommentView implements IRenderedElement { * Creates the top bar and the elements visually within it. * Registers event listeners. */ - private createTopBar( - svgRoot: SVGGElement, - workspace: WorkspaceSvg, - ): { + private createTopBar(svgRoot: SVGGElement): { topBarGroup: SVGGElement; topBarBackground: SVGRectElement; - deleteIcon: SVGImageElement; - foldoutIcon: SVGImageElement; + deleteButton: DeleteCommentBarButton; + foldoutButton: CollapseCommentBarButton; textPreview: SVGTextElement; textPreviewNode: Text; } { @@ -175,24 +172,22 @@ export class CommentView implements IRenderedElement { }, 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`, - }, + const deleteButton = new DeleteCommentBarButton( + this.commentId, + this.workspace, topBarGroup, + this, ); - const foldoutIcon = dom.createSvgElement( - Svg.IMAGE, - { - 'class': 'blocklyFoldoutIcon', - 'href': `${workspace.options.pathToMedia}foldout-icon.svg`, - }, + const foldoutButton = new CollapseCommentBarButton( + this.commentId, + this.workspace, topBarGroup, + this, ); + this.addDisposeListener(() => { + deleteButton.dispose(); + foldoutButton.dispose(); + }); const textPreview = dom.createSvgElement( Svg.TEXT, { @@ -203,27 +198,11 @@ export class CommentView implements IRenderedElement { 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, + deleteButton, + foldoutButton, textPreview, textPreviewNode, }; @@ -232,33 +211,32 @@ export class CommentView implements IRenderedElement { /** * 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, + private createTextArea() { + // When the user is done editing comment, focus the entire comment. + const onFinishEditing = () => this.svgRoot.focus(); + const commentEditor = new CommentEditor( + this.workspace, + this.commentId, + onFinishEditing, ); - 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); + this.svgRoot.appendChild(commentEditor.getDom()); + + commentEditor.addTextChangeListener((oldText, newText) => { + this.updateTextPreview(newText); + // Update size in case our minimum size increased. + this.setSize(this.size); + }); + + return commentEditor; + } - return {foreignObject, textArea}; + /** + * + * @returns The FocusableNode representing the editor portion of this comment. + */ + getEditorFocusableNode(): IFocusableNode { + return this.commentEditor; } /** Creates the DOM elements for the comment resize handle. */ @@ -304,15 +282,10 @@ export class CommentView implements IRenderedElement { */ 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), - ); + size = Size.max(size, this.calcMinSize(topBarSize)); this.size = size; this.svgRoot.setAttribute('height', `${size.height}`); @@ -320,16 +293,10 @@ export class CommentView implements IRenderedElement { 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.commentEditor.updateSize(size, topBarSize); + this.deleteButton.reposition(); + this.foldoutButton.reposition(); + this.updateTextPreviewSize(size, topBarSize, textPreviewSize); this.updateResizeHandlePosition(size, resizeSize); } @@ -351,25 +318,18 @@ export class CommentView implements IRenderedElement { * * 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 ?? ''); + private calcMinSize(topBarSize: Size): Size { + this.updateTextPreview(this.commentEditor.getText() ?? ''); 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; + if (this.foldoutButton.isVisible()) { + width += this.foldoutButton.getSize(true).getWidth(); } else if (textPreviewWidth) { width += 4; // Arbitrary margin before text. } - if (this.deleteIcon.checkVisibility()) { - width += deleteSize.width + deleteMargin * 2; + if (this.deleteButton.isVisible()) { + width += this.deleteButton.getSize(true).getWidth(); } else if (textPreviewWidth) { width += 4; // Arbitrary margin after text. } @@ -380,16 +340,6 @@ export class CommentView implements IRenderedElement { 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}`); @@ -404,44 +354,6 @@ export class CommentView implements IRenderedElement { 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. */ @@ -449,24 +361,16 @@ export class CommentView implements IRenderedElement { 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 foldoutSize = this.foldoutButton.getSize(true); + const deleteSize = this.deleteButton.getSize(true); const textPreviewWidth = - size.width - - foldoutSize.width - - foldoutMargin * 2 - - deleteSize.width - - deleteMargin * 2; + size.width - foldoutSize.getWidth() - deleteSize.getWidth(); this.textPreview.setAttribute( 'x', - `${ - foldoutSize.width + foldoutMargin * 2 * (this.workspace.RTL ? -1 : 1) - }`, + `${(this.workspace.RTL ? -1 : 1) * foldoutSize.getWidth()}`, ); this.textPreview.setAttribute( 'y', @@ -618,25 +522,6 @@ export class CommentView implements IRenderedElement { ); } - /** - * 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; @@ -648,12 +533,11 @@ export class CommentView implements IRenderedElement { 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'); } + this.commentEditor.setEditable(editable); } /** Returns the current location of the comment in workspace coordinates. */ @@ -674,44 +558,29 @@ export class CommentView implements IRenderedElement { ); } - /** Retursn the current text of the comment. */ + /** Returns the current text of the comment. */ getText() { - return this.text; + return this.commentEditor.getText(); } /** Sets the current text of the comment. */ setText(text: string) { - this.textArea.value = text; - this.onTextChange(); + this.commentEditor.setText(text); } - /** Registers a callback that listens for text changes. */ - addTextChangeListener(listener: (oldText: string, newText: string) => void) { - this.textChangeListeners.push(listener); + /** Sets the placeholder text displayed for an empty comment. */ + setPlaceholderText(text: string) { + this.commentEditor.setPlaceholderText(text); } - /** Removes the given listener from the list of text change listeners. */ - removeTextChangeListener(listener: () => void) { - this.textChangeListeners.splice( - this.textChangeListeners.indexOf(listener), - 1, - ); + /** Registers a callback that listens for text changes on the comment editor. */ + addTextChangeListener(listener: (oldText: string, newText: string) => void) { + this.commentEditor.addTextChangeListener(listener); } - /** - * 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); - } + /** Removes the given listener from the comment editor. */ + removeTextChangeListener(listener: () => void) { + this.commentEditor.removeTextChangeListener(listener); } /** Updates the preview text element to reflect the given text. */ @@ -725,7 +594,7 @@ export class CommentView implements IRenderedElement { } /** Brings the workspace comment to the front of its layer. */ - private bringToFront() { + bringToFront() { const parent = this.svgRoot.parentNode; const childNodes = parent!.childNodes; // Avoid moving the comment if it's already at the bottom. @@ -757,6 +626,7 @@ export class CommentView implements IRenderedElement { for (let i = this.disposeListeners.length - 1; i >= 0; i--) { this.disposeListeners[i](); } + this.disposeListeners.length = 0; this.disposed = true; } @@ -782,6 +652,13 @@ export class CommentView implements IRenderedElement { removeDisposeListener(listener: () => void) { this.disposeListeners.splice(this.disposeListeners.indexOf(listener), 1); } + + /** + * @internal + */ + getCommentBarButtons(): CommentBarButton[] { + return [this.foldoutButton, this.deleteButton]; + } } css.register(` @@ -875,6 +752,11 @@ css.register(` fill: none; } +.blocklyCommentText.blocklyActiveFocus { + border-color: #fc3; + border-width: 2px; +} + .blocklySelected .blocklyCommentHighlight { stroke: #fc3; stroke-width: 3px; diff --git a/packages/blockly/core/comments/delete_comment_bar_button.ts b/packages/blockly/core/comments/delete_comment_bar_button.ts new file mode 100644 index 00000000000..c61db9b9cd2 --- /dev/null +++ b/packages/blockly/core/comments/delete_comment_bar_button.ts @@ -0,0 +1,106 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as browserEvents from '../browser_events.js'; +import {getFocusManager} from '../focus_manager.js'; +import * as touch from '../touch.js'; +import * as dom from '../utils/dom.js'; +import {Svg} from '../utils/svg.js'; +import type {WorkspaceSvg} from '../workspace_svg.js'; +import {CommentBarButton} from './comment_bar_button.js'; +import type {CommentView} from './comment_view.js'; + +/** + * Magic string appended to the comment ID to create a unique ID for this button. + */ +export const COMMENT_DELETE_BAR_BUTTON_FOCUS_IDENTIFIER = '_delete_bar_button'; + +/** + * Button that deletes a comment. + */ +export class DeleteCommentBarButton extends CommentBarButton { + /** + * Opaque ID used to unbind event handlers during disposal. + */ + private readonly bindId: browserEvents.Data; + + /** + * SVG image displayed on this button. + */ + protected override readonly icon: SVGImageElement; + + /** + * Creates a new DeleteCommentBarButton instance. + * + * @param id The ID of this button's parent comment. + * @param workspace The workspace this button's parent comment is shown on. + * @param container An SVG group that this button should be a child of. + */ + constructor( + protected readonly id: string, + protected readonly workspace: WorkspaceSvg, + protected readonly container: SVGGElement, + protected readonly commentView: CommentView, + ) { + super(id, workspace, container, commentView); + + this.icon = dom.createSvgElement( + Svg.IMAGE, + { + 'class': 'blocklyDeleteIcon', + 'href': `${this.workspace.options.pathToMedia}delete-icon.svg`, + 'id': `${this.id}${COMMENT_DELETE_BAR_BUTTON_FOCUS_IDENTIFIER}`, + }, + container, + ); + this.bindId = browserEvents.conditionalBind( + this.icon, + 'pointerdown', + this, + this.performAction.bind(this), + ); + } + + /** + * Disposes of this button. + */ + dispose() { + browserEvents.unbind(this.bindId); + } + + /** + * Adjusts the positioning of this button within its container. + */ + override reposition() { + const margin = this.getMargin(); + // Reset to 0 so that our position doesn't force the parent container to + // grow. + this.icon.setAttribute('x', `0`); + const containerSize = this.container.getBBox(); + this.icon.setAttribute('y', `${margin}`); + this.icon.setAttribute( + 'x', + `${containerSize.width - this.getSize(true).getWidth()}`, + ); + } + + /** + * Deletes parent comment. + * + * @param e The event that triggered this action. + */ + override performAction(e?: Event) { + touch.clearTouchIdentifier(); + if (e && e instanceof PointerEvent && browserEvents.isRightButton(e)) { + e.stopPropagation(); + return; + } + + this.getCommentView().dispose(); + e?.stopPropagation(); + getFocusManager().focusNode(this.workspace); + } +} diff --git a/core/comments/rendered_workspace_comment.ts b/packages/blockly/core/comments/rendered_workspace_comment.ts similarity index 75% rename from core/comments/rendered_workspace_comment.ts rename to packages/blockly/core/comments/rendered_workspace_comment.ts index f4885df46f7..59e462c9507 100644 --- a/core/comments/rendered_workspace_comment.ts +++ b/packages/blockly/core/comments/rendered_workspace_comment.ts @@ -13,19 +13,22 @@ 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 {getFocusManager} from '../focus_manager.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 {IFocusableNode} from '../interfaces/i_focusable_node.js'; +import type {IFocusableTree} from '../interfaces/i_focusable_tree.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 * as svgMath from '../utils/svg_math.js'; import {WorkspaceSvg} from '../workspace_svg.js'; import {CommentView} from './comment_view.js'; import {WorkspaceComment} from './workspace_comment.js'; @@ -39,10 +42,11 @@ export class RenderedWorkspaceComment ISelectable, IDeletable, ICopyable, - IContextMenu + IContextMenu, + IFocusableNode { /** The class encompassing the svg elements making up the workspace comment. */ - private view: CommentView; + view: CommentView; public readonly workspace: WorkspaceSvg; @@ -54,11 +58,12 @@ export class RenderedWorkspaceComment this.workspace = workspace; - this.view = new CommentView(workspace); + this.view = new CommentView(workspace, this.id); // 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.view.getSvgRoot().setAttribute('id', this.id); this.addModelUpdateBindings(); @@ -68,15 +73,6 @@ export class RenderedWorkspaceComment this, this.startGesture, ); - // Don't zoom with mousewheel; let it scroll instead. - browserEvents.conditionalBind( - this.view.getSvgRoot(), - 'wheel', - this, - (e: Event) => { - e.stopPropagation(); - }, - ); } /** @@ -105,6 +101,11 @@ export class RenderedWorkspaceComment this.view.setText(text); } + /** Sets the placeholder text displayed if the comment is empty. */ + setPlaceholderText(text: string): void { + this.view.setPlaceholderText(text); + } + /** Sets the size of the comment. */ override setSize(size: Size) { // setSize will trigger the change listener that updates @@ -197,7 +198,12 @@ export class RenderedWorkspaceComment /** Disposes of the view. */ override dispose() { this.disposing = true; + const focusManager = getFocusManager(); + if (focusManager.getFocusedNode() === this) { + setTimeout(() => focusManager.focusTree(this.workspace), 0); + } if (!this.view.isDeadOrDying()) this.view.dispose(); + super.dispose(); } @@ -208,15 +214,8 @@ export class RenderedWorkspaceComment private startGesture(e: PointerEvent) { const gesture = this.workspace.getGesture(e); if (gesture) { - if (browserEvents.isTargetInput(e)) { - // If the text area was the focus, don't allow this event to bubble up - // and steal focus away from the editor/comment. - e.stopPropagation(); - } else { - gesture.handleCommentStart(e, this); - this.workspace.getLayerManager()?.append(this, layers.BLOCK); - } - common.setSelected(this); + gesture.handleCommentStart(e, this); + getFocusManager().focusNode(this); } } @@ -229,6 +228,11 @@ export class RenderedWorkspaceComment } } + /** Returns whether this comment is copyable or not */ + isCopyable(): boolean { + return this.isOwnMovable() && this.isOwnDeletable(); + } + /** Returns whether this comment is movable or not. */ isMovable(): boolean { return this.dragStrategy.isMovable(); @@ -257,11 +261,13 @@ export class RenderedWorkspaceComment /** Visually highlights the comment. */ select(): void { dom.addClass(this.getSvgRoot(), 'blocklySelected'); + common.fireSelectedEvent(this); } /** Visually unhighlights the comment. */ unselect(): void { dom.removeClass(this.getSvgRoot(), 'blocklySelected'); + common.fireSelectedEvent(null); } /** @@ -273,17 +279,37 @@ export class RenderedWorkspaceComment paster: WorkspaceCommentPaster.TYPE, commentState: commentSerialization.save(this, { addCoordinates: true, + saveIds: false, }), }; } /** Show a context menu for this comment. */ - showContextMenu(e: PointerEvent): void { + showContextMenu(e: Event): void { const menuOptions = ContextMenuRegistry.registry.getContextMenuOptions( - ContextMenuRegistry.ScopeType.COMMENT, - {comment: this}, + {comment: this, focusedNode: this}, + e, + ); + + let location: Coordinate; + if (e instanceof PointerEvent) { + location = new Coordinate(e.clientX, e.clientY); + } else { + // Show the menu based on the location of the comment + const xy = svgMath.wsToScreenCoordinates( + this.workspace, + this.getRelativeToSurfaceXY(), + ); + location = xy.translate(10, 10); + } + + contextMenu.show( + e, + menuOptions, + this.workspace.RTL, + this.workspace, + location, ); - contextMenu.show(e, menuOptions, this.workspace.RTL, this.workspace); } /** Snap this comment to the nearest grid point. */ @@ -297,4 +323,39 @@ export class RenderedWorkspaceComment this.moveTo(alignedXY, ['snap']); } } + + /** + * @returns The FocusableNode representing the editor portion of this comment. + */ + getEditorFocusableNode(): IFocusableNode { + return this.view.getEditorFocusableNode(); + } + + /** See IFocusableNode.getFocusableElement. */ + getFocusableElement(): HTMLElement | SVGElement { + return this.getSvgRoot(); + } + + /** See IFocusableNode.getFocusableTree. */ + getFocusableTree(): IFocusableTree { + return this.workspace; + } + + /** See IFocusableNode.onNodeFocus. */ + onNodeFocus(): void { + this.select(); + // Ensure that the comment is always at the top when focused. + this.getSvgRoot().parentElement?.appendChild(this.getSvgRoot()); + this.workspace.scrollBoundsIntoView(this.getBoundingRectangle()); + } + + /** See IFocusableNode.onNodeBlur. */ + onNodeBlur(): void { + this.unselect(); + } + + /** See IFocusableNode.canBeFocused. */ + canBeFocused(): boolean { + return true; + } } diff --git a/core/comments/workspace_comment.ts b/packages/blockly/core/comments/workspace_comment.ts similarity index 94% rename from core/comments/workspace_comment.ts rename to packages/blockly/core/comments/workspace_comment.ts index 2d59c715edd..b5dc3023cfe 100644 --- a/core/comments/workspace_comment.ts +++ b/packages/blockly/core/comments/workspace_comment.ts @@ -12,6 +12,7 @@ import {Coordinate} from '../utils/coordinate.js'; import * as idGenerator from '../utils/idgenerator.js'; import {Size} from '../utils/size.js'; import {Workspace} from '../workspace.js'; +import {CommentView} from './comment_view.js'; export class WorkspaceComment { /** The unique identifier for this comment. */ @@ -21,7 +22,7 @@ export class WorkspaceComment { private text = ''; /** The size of the comment in workspace units. */ - private size = new Size(120, 100); + private size: Size; /** Whether the comment is collapsed or not. */ private collapsed = false; @@ -56,6 +57,7 @@ export class WorkspaceComment { id?: string, ) { this.id = id && !workspace.getCommentById(id) ? id : idGenerator.genUid(); + this.size = CommentView.defaultCommentSize; workspace.addTopComment(this); @@ -142,7 +144,7 @@ export class WorkspaceComment { * workspace is read-only. */ isEditable(): boolean { - return this.isOwnEditable() && !this.workspace.options.readOnly; + return this.isOwnEditable() && !this.workspace.isReadOnly(); } /** @@ -163,7 +165,11 @@ export class WorkspaceComment { * workspace is read-only. */ isMovable() { - return this.isOwnMovable() && !this.workspace.options.readOnly; + return ( + this.isOwnMovable() && + !this.workspace.isReadOnly() && + !this.workspace.isFlyout + ); } /** @@ -187,7 +193,8 @@ export class WorkspaceComment { return ( this.isOwnDeletable() && !this.isDeadOrDying() && - !this.workspace.options.readOnly + !this.workspace.isReadOnly() && + !this.workspace.isFlyout ); } diff --git a/core/common.ts b/packages/blockly/core/common.ts similarity index 78% rename from core/common.ts rename to packages/blockly/core/common.ts index bc31bf17eea..7f23779ec93 100644 --- a/core/common.ts +++ b/packages/blockly/core/common.ts @@ -7,11 +7,14 @@ // Former goog.module ID: Blockly.common import type {Block} from './block.js'; -import {ISelectable} from './blockly.js'; import {BlockDefinition, Blocks} from './blocks.js'; +import * as browserEvents from './browser_events.js'; import type {Connection} from './connection.js'; import {EventType} from './events/type.js'; import * as eventUtils from './events/utils.js'; +import {getFocusManager} from './focus_manager.js'; +import {ISelectable, isSelectable} from './interfaces/i_selectable.js'; +import {ShortcutRegistry} from './shortcut_registry.js'; import type {Workspace} from './workspace.js'; import type {WorkspaceSvg} from './workspace_svg.js'; @@ -86,38 +89,45 @@ export function setMainWorkspace(workspace: Workspace) { } /** - * Currently selected copyable object. + * Returns the current selection. */ -let selected: ISelectable | null = null; +export function getSelected(): ISelectable | null { + const focused = getFocusManager().getFocusedNode(); + if (focused && isSelectable(focused)) return focused; + return null; +} /** - * Returns the currently selected copyable object. + * Sets the current selection. + * + * To clear the current selection, select another ISelectable or focus a + * non-selectable (like the workspace root node). + * + * @param newSelection The new selection to make. + * @internal */ -export function getSelected(): ISelectable | null { - return selected; +export function setSelected(newSelection: ISelectable) { + getFocusManager().focusNode(newSelection); } /** - * 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`. + * Fires a selection change event based on the new selection. + * + * This is only expected to be called by ISelectable implementations and should + * always be called before updating the current selection state. It does not + * change focus or selection state. * - * @param newSelection The newly selected block. + * @param newSelection The new selection. * @internal */ -export function setSelected(newSelection: ISelectable | null) { - if (selected === newSelection) return; - +export function fireSelectedEvent(newSelection: ISelectable | null) { + const selected = getSelected(); 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(); } /** @@ -302,4 +312,36 @@ export function defineBlocks(blocks: {[key: string]: BlockDefinition}) { } } +/** + * Handle a key-down on SVG drawing surface. Does nothing if the main workspace + * is not visible. + * + * @internal + * @param e Key down event. + */ +export function globalShortcutHandler(e: KeyboardEvent) { + // This would ideally just be a `focusedTree instanceof WorkspaceSvg`, but + // importing `WorkspaceSvg` (as opposed to just its type) causes cycles. + let workspace: WorkspaceSvg = getMainWorkspace() as WorkspaceSvg; + const focusedTree = getFocusManager().getFocusedTree(); + for (const ws of getAllWorkspaces()) { + if (focusedTree === (ws as WorkspaceSvg)) { + workspace = ws as WorkspaceSvg; + break; + } + } + + if ( + browserEvents.isTargetInput(e) || + !workspace || + (workspace.rendered && !workspace.isFlyout && !workspace.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(workspace, e); +} + export const TEST_ONLY = {defineBlocksWithJsonArrayInternal}; diff --git a/core/component_manager.ts b/packages/blockly/core/component_manager.ts similarity index 100% rename from core/component_manager.ts rename to packages/blockly/core/component_manager.ts diff --git a/core/config.ts b/packages/blockly/core/config.ts similarity index 100% rename from core/config.ts rename to packages/blockly/core/config.ts diff --git a/core/connection.ts b/packages/blockly/core/connection.ts similarity index 97% rename from core/connection.ts rename to packages/blockly/core/connection.ts index 9cc2c28a923..a55c2505915 100644 --- a/core/connection.ts +++ b/packages/blockly/core/connection.ts @@ -17,15 +17,15 @@ 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 {idGenerator} from './utils.js'; import * as Xml from './xml.js'; /** * Class for a connection between blocks. */ -export class Connection implements IASTNodeLocationWithBlock { +export class Connection { /** Constants for checking whether two connections are compatible. */ static CAN_CONNECT = 0; static REASON_SELF_CONNECTION = 1; @@ -55,6 +55,9 @@ export class Connection implements IASTNodeLocationWithBlock { /** DOM representation of a shadow block, or null if none. */ private shadowDom: Element | null = null; + /** The unique ID of this connection. */ + id: string; + /** * Horizontal location of this connection. * @@ -80,6 +83,13 @@ export class Connection implements IASTNodeLocationWithBlock { public type: number, ) { this.sourceBlock_ = source; + if (source.id.includes('_connection')) { + throw new Error( + `Connection ID indicator is contained in block ID. This will cause ` + + `problems with focus: ${source.id}.`, + ); + } + this.id = `${source.id}_connection_${idGenerator.getNextUniqueId()}`; } /** @@ -281,7 +291,10 @@ export class Connection implements IASTNodeLocationWithBlock { } let event; - if (eventUtils.isEnabled()) { + if ( + eventUtils.isEnabled() && + !childConnection.getSourceBlock().isDeadOrDying() + ) { event = new (eventUtils.get(EventType.BLOCK_MOVE))( childConnection.getSourceBlock(), ) as BlockMove; @@ -485,7 +498,7 @@ export class Connection implements IASTNodeLocationWithBlock { * * 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 + * {@link (RenderedConnection:class).neighbours} overrides this behavior with a list * computed from the rendered positioning. * * @param _maxLimit The maximum radius to another connection. diff --git a/core/connection_checker.ts b/packages/blockly/core/connection_checker.ts similarity index 100% rename from core/connection_checker.ts rename to packages/blockly/core/connection_checker.ts diff --git a/core/connection_db.ts b/packages/blockly/core/connection_db.ts similarity index 100% rename from core/connection_db.ts rename to packages/blockly/core/connection_db.ts diff --git a/core/connection_type.ts b/packages/blockly/core/connection_type.ts similarity index 100% rename from core/connection_type.ts rename to packages/blockly/core/connection_type.ts diff --git a/core/constants.ts b/packages/blockly/core/constants.ts similarity index 100% rename from core/constants.ts rename to packages/blockly/core/constants.ts diff --git a/core/contextmenu.ts b/packages/blockly/core/contextmenu.ts similarity index 79% rename from core/contextmenu.ts rename to packages/blockly/core/contextmenu.ts index b49dcba51c0..f3ebbd1c681 100644 --- a/core/contextmenu.ts +++ b/packages/blockly/core/contextmenu.ts @@ -9,7 +9,6 @@ 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, @@ -17,10 +16,13 @@ import type { } from './contextmenu_registry.js'; import {EventType} from './events/type.js'; import * as eventUtils from './events/utils.js'; +import {getFocusManager} from './focus_manager.js'; import {Menu} from './menu.js'; +import {MenuSeparator} from './menu_separator.js'; import {MenuItem} from './menuitem.js'; import * as serializationBlocks from './serialization/blocks.js'; import * as aria from './utils/aria.js'; +import {Coordinate} from './utils/coordinate.js'; import * as dom from './utils/dom.js'; import {Rect} from './utils/rect.js'; import * as svgMath from './utils/svg_math.js'; @@ -37,6 +39,8 @@ const dummyOwner = {}; /** * Gets the block the context menu is currently attached to. + * It is not recommended that you use this function; instead, + * use the scope object passed to the context menu callback. * * @returns The block the context menu is attached to. */ @@ -61,26 +65,38 @@ let menu_: Menu | null = null; /** * Construct the menu based on the list of options and show the menu. * - * @param e Mouse event. + * @param menuOpenEvent Event that caused the menu to open. * @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. + * @param location The screen coordinates at which to show the menu. */ export function show( - e: PointerEvent, + menuOpenEvent: Event, options: (ContextMenuOption | LegacyContextMenuOption)[], rtl: boolean, workspace?: WorkspaceSvg, + location?: Coordinate, ) { WidgetDiv.show(dummyOwner, rtl, dispose, workspace); if (!options.length) { hide(); return; } - const menu = populate_(options, rtl, e); + + if (!location) { + if (menuOpenEvent instanceof PointerEvent) { + location = new Coordinate(menuOpenEvent.clientX, menuOpenEvent.clientY); + } else { + // We got a keyboard event that didn't tell us where to open the menu, so just guess + console.warn('Context menu opened with keyboard but no location given'); + location = new Coordinate(0, 0); + } + } + const menu = populate_(options, rtl, menuOpenEvent, location); menu_ = menu; - position_(menu, e, rtl); + position_(menu, rtl, location); // 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 () { @@ -94,13 +110,15 @@ export function show( * * @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. + * @param menuOpenEvent The event that triggered the context menu to open. + * @param location The screen coordinates at which to show the menu. * @returns The menu that will be shown on right click. */ function populate_( options: (ContextMenuOption | LegacyContextMenuOption)[], rtl: boolean, - e: PointerEvent, + menuOpenEvent: Event, + location: Coordinate, ): Menu { /* Here's what one option object looks like: {text: 'Make It So', @@ -111,13 +129,18 @@ function populate_( menu.setRole(aria.Role.MENU); for (let i = 0; i < options.length; i++) { const option = options[i]; + if (option.separator) { + menu.addChild(new MenuSeparator()); + continue; + } + 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 () { + const actionHandler = function (p1: MenuItem, menuSelectEvent: Event) { hide(); requestAnimationFrame(() => { setTimeout(() => { @@ -125,7 +148,12 @@ function populate_( // 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); + option.callback( + (option as ContextMenuOption).scope, + menuOpenEvent, + menuSelectEvent, + location, + ); }, 0); }); }; @@ -139,21 +167,19 @@ function populate_( * 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. + * @param location The location at which to anchor the menu. */ -function position_(menu: Menu, e: Event, rtl: boolean) { +function position_(menu: Menu, rtl: boolean, location: Coordinate) { // 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, + location.y + viewportBBox.top, + location.y + viewportBBox.top, + location.x + viewportBBox.left, + location.x + viewportBBox.left, ); createWidget_(menu); @@ -263,7 +289,7 @@ export function callbackFactory( if (eventUtils.isEnabled() && !newBlock.isShadow()) { eventUtils.fire(new (eventUtils.get(EventType.BLOCK_CREATE))(newBlock)); } - common.setSelected(newBlock); + getFocusManager().focusNode(newBlock); return newBlock; }; } diff --git a/core/contextmenu_items.ts b/packages/blockly/core/contextmenu_items.ts similarity index 91% rename from core/contextmenu_items.ts rename to packages/blockly/core/contextmenu_items.ts index 58429fb1381..8e42284dd2a 100644 --- a/core/contextmenu_items.ts +++ b/packages/blockly/core/contextmenu_items.ts @@ -9,7 +9,6 @@ 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, @@ -19,12 +18,20 @@ import { import * as dialog from './dialog.js'; import * as Events from './events/events.js'; import * as eventUtils from './events/utils.js'; +import {getFocusManager} from './focus_manager.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 * as svgMath from './utils/svg_math.js'; import type {WorkspaceSvg} from './workspace_svg.js'; +function isFullBlockField(block?: BlockSvg) { + if (!block || !block.isSimpleReporter()) return false; + const firstField = block.getFields().next().value; + return firstField?.isFullBlockField(); +} + /** * Option to undo previous action. */ @@ -362,10 +369,15 @@ export function registerComment() { preconditionFn(scope: Scope) { const block = scope.block; if ( - !block!.isInFlyout && - block!.workspace.options.comments && - !block!.isCollapsed() && - block!.isEditable() + block && + !block.isInFlyout && + block.workspace.options.comments && + !block.isCollapsed() && + block.isEditable() && + // Either block already has a comment so let us remove it, + // or the block isn't just one full-block field block, which + // shouldn't be allowed to have comments as there's no way to read them. + (block.hasIcon(CommentIcon.TYPE) || !isFullBlockField(block)) ) { return 'enabled'; } @@ -373,8 +385,8 @@ export function registerComment() { }, callback(scope: Scope) { const block = scope.block; - if (block!.hasIcon(CommentIcon.TYPE)) { - block!.setCommentText(null); + if (block && block.hasIcon(CommentIcon.TYPE)) { + block.setCommentText(null); } else { block!.setCommentText(''); } @@ -614,19 +626,23 @@ export function registerCommentCreate() { preconditionFn: (scope: Scope) => { return scope.workspace?.isMutator ? 'hidden' : 'enabled'; }, - callback: (scope: Scope, e: PointerEvent) => { + callback: ( + scope: Scope, + menuOpenEvent: Event, + menuSelectEvent: Event, + location: Coordinate, + ) => { 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), + svgMath.screenToWsCoordinates( workspace, + new Coordinate(location.x, location.y), ), ); - common.setSelected(comment); + getFocusManager().focusNode(comment); eventUtils.setGroup(false); }, scopeType: ContextMenuRegistry.ScopeType.WORKSPACE, @@ -636,40 +652,6 @@ export function registerCommentCreate() { 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(); diff --git a/core/contextmenu_registry.ts b/packages/blockly/core/contextmenu_registry.ts similarity index 51% rename from core/contextmenu_registry.ts rename to packages/blockly/core/contextmenu_registry.ts index fb0d899d141..5b84104c101 100644 --- a/core/contextmenu_registry.ts +++ b/packages/blockly/core/contextmenu_registry.ts @@ -13,6 +13,8 @@ import type {BlockSvg} from './block_svg.js'; import {RenderedWorkspaceComment} from './comments/rendered_workspace_comment.js'; +import type {IFocusableNode} from './interfaces/i_focusable_node.js'; +import {Coordinate} from './utils/coordinate.js'; import type {WorkspaceSvg} from './workspace_svg.js'; /** @@ -70,39 +72,60 @@ export class ContextMenuRegistry { } /** - * 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 + * Gets the valid context menu options for the given scope. + * Options are only included 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) + * block being clicked on). + * @param menuOpenEvent Event that caused the menu to open. * @returns the list of ContextMenuOptions */ getContextMenuOptions( - scopeType: ScopeType, scope: Scope, + menuOpenEvent: Event, ): 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); - } + if (item.scopeType) { + // If the scopeType is present, check to make sure + // that the option is compatible with the current scope + if (item.scopeType === ScopeType.BLOCK && !scope.block) continue; + if (item.scopeType === ScopeType.COMMENT && !scope.comment) continue; + if (item.scopeType === ScopeType.WORKSPACE && !scope.workspace) + continue; } + let menuOption: + | ContextMenuRegistry.CoreContextMenuOption + | ContextMenuRegistry.SeparatorContextMenuOption + | ContextMenuRegistry.ActionContextMenuOption; + menuOption = { + scope, + weight: item.weight, + }; + + const precondition = item.preconditionFn?.(scope, menuOpenEvent); + if (precondition === 'hidden') continue; + + if (item.separator) { + menuOption = { + ...menuOption, + separator: true, + }; + } else { + const displayText = + typeof item.displayText === 'function' + ? item.displayText(scope) + : item.displayText; + menuOption = { + ...menuOption, + text: displayText, + callback: item.callback, + enabled: precondition === 'enabled', + }; + } + + menuOptions.push(menuOption); } menuOptions.sort(function (a, b) { return a.weight - b.weight; @@ -124,50 +147,110 @@ export namespace ContextMenuRegistry { } /** - * The actual workspace/block where the menu is being rendered. This is passed - * to callback and displayText functions that depend on this information. + * The actual workspace/block/focused object 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; + focusedNode?: IFocusableNode; } /** - * A menu item as entered in the registry. + * Fields common to all context menu registry items. */ - export interface RegistryItem { + interface CoreRegistryItem { + scopeType?: ScopeType; + weight: number; + id: string; + preconditionFn?: (p1: Scope, menuOpenEvent: Event) => string; + } + + /** + * A representation of a normal, clickable menu item in the registry. + */ + interface ActionRegistryItem extends CoreRegistryItem { /** * @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. + * @param menuOpenEvent The original event that triggered the context menu to open. + * @param menuSelectEvent The event that triggered the option being selected. + * @param location The location in screen coordinates where the menu was opened. */ - callback: (scope: Scope, e: PointerEvent) => void; - scopeType: ScopeType; + callback: ( + scope: Scope, + menuOpenEvent: Event, + menuSelectEvent: Event, + location: Coordinate, + ) => void; displayText: ((p1: Scope) => string | HTMLElement) | string | HTMLElement; - preconditionFn: (p1: Scope) => string; + separator?: never; + preconditionFn: (p1: Scope, menuOpenEvent: Event) => string; + } + + /** + * A representation of a menu separator item in the registry. + */ + interface SeparatorRegistryItem extends CoreRegistryItem { + separator: true; + callback?: never; + displayText?: never; + } + + /** + * A menu item as entered in the registry. + */ + export type RegistryItem = ActionRegistryItem | SeparatorRegistryItem; + + /** + * Fields common to all context menu items as used by contextmenu.ts. + */ + export interface CoreContextMenuOption { + scope: Scope; weight: number; - id: string; } /** - * A menu item as presented to contextmenu.js. + * A representation of a normal, clickable menu item in contextmenu.ts. */ - export interface ContextMenuOption { + export interface ActionContextMenuOption extends CoreContextMenuOption { 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. + * @param menuOpenEvent The original event that triggered the context menu to open. + * @param menuSelectEvent The event that triggered the option being selected. + * @param location The location in screen coordinates where the menu was opened. */ - callback: (scope: Scope, e: PointerEvent) => void; - scope: Scope; - weight: number; + callback: ( + scope: Scope, + menuOpenEvent: Event, + menuSelectEvent: Event, + location: Coordinate, + ) => void; + separator?: never; + } + + /** + * A representation of a menu separator item in contextmenu.ts. + */ + export interface SeparatorContextMenuOption extends CoreContextMenuOption { + separator: true; + text?: never; + enabled?: never; + callback?: never; } + /** + * A menu item as presented to contextmenu.ts. + */ + export type ContextMenuOption = + | ActionContextMenuOption + | SeparatorContextMenuOption; + /** * A subset of ContextMenuOption corresponding to what was publicly * documented. ContextMenuOption should be preferred for new code. @@ -176,6 +259,7 @@ export namespace ContextMenuRegistry { text: string; enabled: boolean; callback: (p1: Scope) => void; + separator?: never; } /** diff --git a/core/css.ts b/packages/blockly/core/css.ts similarity index 86% rename from core/css.ts rename to packages/blockly/core/css.ts index d0e06704162..de9e682f87e 100644 --- a/core/css.ts +++ b/packages/blockly/core/css.ts @@ -5,7 +5,6 @@ */ // Former goog.module ID: Blockly.Css - /** Has CSS already been injected? */ let injected = false; @@ -83,15 +82,20 @@ let content = ` -webkit-user-select: none; } -.blocklyNonSelectable { - user-select: none; - -ms-user-select: none; - -webkit-user-select: none; -} - .blocklyBlockCanvas.blocklyCanvasTransitioning, .blocklyBubbleCanvas.blocklyCanvasTransitioning { - transition: transform .5s; + transition: transform .15s; +} + +@media (prefers-reduced-motion) { + .blocklyBlockCanvas.blocklyCanvasTransitioning, + .blocklyBubbleCanvas.blocklyCanvasTransitioning { + transition: none; + } +} + +.blocklyEmboss { + filter: var(--blocklyEmbossFilter); } .blocklyTooltipDiv { @@ -121,15 +125,12 @@ let content = ` box-shadow: 0 0 3px 1px rgba(0,0,0,.3); } -.blocklyDropDownDiv.blocklyFocused { +.blocklyDropDownDiv:focus { 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 { @@ -141,47 +142,14 @@ let content = ` 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; +.blocklyHighlighted>.blocklyPath { + filter: var(--blocklyEmbossFilter); } .blocklyHighlightedConnectionPath { @@ -220,7 +188,8 @@ let content = ` cursor: -webkit-grabbing; } -.blocklyDragging.blocklyDraggingDelete { +.blocklyDragging.blocklyDraggingDelete, +.blocklyDragging.blocklyDraggingDelete .blocklyField { cursor: url("<<>>/handdelete.cur"), auto; } @@ -234,7 +203,8 @@ let content = ` display: none; } -.blocklyDisabled>.blocklyPath { +.blocklyDisabledPattern>.blocklyPath { + fill: var(--blocklyDisabledPattern); fill-opacity: .5; stroke-opacity: .5; } @@ -251,7 +221,7 @@ let content = ` stroke: none; } -.blocklyNonEditableText>text { +.blocklyNonEditableField>text { pointer-events: none; } @@ -264,19 +234,22 @@ let content = ` cursor: default; } -.blocklyHidden { - display: none; -} - -.blocklyFieldDropdown:not(.blocklyHidden) { - display: block; +/* + Don't allow users to select text. It gets annoying when trying to + drag a block and selected text moves instead. +*/ +.blocklySvg text { + user-select: none; + -ms-user-select: none; + -webkit-user-select: none; + cursor: inherit; } .blocklyIconGroup { cursor: default; } -.blocklyIconGroup:not(:hover), +.blocklyIconGroup:not(:hover):not(:focus), .blocklyIconGroupReadonly { opacity: .6; } @@ -419,6 +392,9 @@ input[type=number] { } .blocklyWidgetDiv .blocklyMenu { + user-select: none; + -ms-user-select: none; + -webkit-user-select: none; background: #fff; border: 1px solid transparent; box-shadow: 0 0 3px 1px rgba(0,0,0,.3); @@ -433,16 +409,21 @@ input[type=number] { z-index: 20000; /* Arbitrary, but some apps depend on it... */ } -.blocklyWidgetDiv .blocklyMenu.blocklyFocused { +.blocklyWidgetDiv .blocklyMenu:focus { box-shadow: 0 0 6px 1px rgba(0,0,0,.3); } .blocklyDropDownDiv .blocklyMenu { + user-select: none; + -ms-user-select: none; + -webkit-user-select: none; 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 */ + overflow-y: auto; + overflow-x: hidden; + max-height: 100%; z-index: 20000; /* Arbitrary, but some apps depend on it... */ } @@ -489,6 +470,14 @@ input[type=number] { margin-right: -24px; } +.blocklyMenuSeparator { + background-color: #ccc; + height: 1px; + border: 0; + margin-left: 4px; + margin-right: 4px; +} + .blocklyBlockDragSurface, .blocklyAnimationLayer { position: absolute; top: 0; @@ -499,4 +488,31 @@ input[type=number] { z-index: 80; pointer-events: none; } + +.blocklyField { + cursor: default; +} + +.blocklyInputField { + cursor: text; +} + +.blocklyDragging .blocklyField, +.blocklyDragging .blocklyIconGroup { + cursor: grabbing; +} + +.blocklyActiveFocus:is( + .blocklyFlyout, + .blocklyWorkspace, + .blocklyField, + .blocklyPath, + .blocklyHighlightedConnectionPath, + .blocklyComment, + .blocklyBubble, + .blocklyIconGroup, + .blocklyTextarea +) { + outline: none; +} `; diff --git a/core/delete_area.ts b/packages/blockly/core/delete_area.ts similarity index 100% rename from core/delete_area.ts rename to packages/blockly/core/delete_area.ts diff --git a/packages/blockly/core/dialog.ts b/packages/blockly/core/dialog.ts new file mode 100644 index 00000000000..96631e9cbc7 --- /dev/null +++ b/packages/blockly/core/dialog.ts @@ -0,0 +1,167 @@ +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.dialog + +import type {ToastOptions} from './toast.js'; +import {Toast} from './toast.js'; +import type {WorkspaceSvg} from './workspace_svg.js'; + +const defaultAlert = function (message: string, opt_callback?: () => void) { + window.alert(message); + if (opt_callback) { + opt_callback(); + } +}; + +let alertImplementation = defaultAlert; + +const defaultConfirm = function ( + message: string, + callback: (result: boolean) => void, +) { + callback(window.confirm(message)); +}; + +let confirmImplementation = defaultConfirm; + +const defaultPrompt = function ( + message: string, + defaultValue: string, + callback: (result: string | null) => void, +) { + // NOTE TO DEVELOPER: Ephemeral focus doesn't need to be taken for the native + // window prompt since it prevents focus from changing while open. + callback(window.prompt(message, defaultValue)); +}; + +let promptImplementation = defaultPrompt; + +const defaultToast = Toast.show.bind(Toast); +let toastImplementation = defaultToast; + +/** + * 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, or undefined to restore the + * default implementation. + * @see Blockly.dialog.alert + */ +export function setAlert( + alertFunction: ( + message: string, + callback?: () => void, + ) => void = defaultAlert, +) { + 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: (result: boolean) => void) { + confirmImplementation(message, callback); +} + +/** + * Sets the function to be run when Blockly.dialog.confirm() is called. + * + * @param confirmFunction The function to be run, or undefined to restore the + * default implementation. + * @see Blockly.dialog.confirm + */ +export function setConfirm( + confirmFunction: ( + message: string, + callback: (result: boolean) => void, + ) => void = defaultConfirm, +) { + 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: (result: string | null) => void, +) { + promptImplementation(message, defaultValue, callback); +} + +/** + * Sets the function to be run when Blockly.dialog.prompt() is called. + * + * **Important**: When overridding this, be aware that non-native prompt + * experiences may require managing ephemeral focus in FocusManager. This isn't + * needed for the native window prompt because it prevents focus from being + * changed while open. + * + * @param promptFunction The function to be run, or undefined to restore the + * default implementation. + * @see Blockly.dialog.prompt + */ +export function setPrompt( + promptFunction: ( + message: string, + defaultValue: string, + callback: (result: string | null) => void, + ) => void = defaultPrompt, +) { + promptImplementation = promptFunction; +} + +/** + * Displays a temporary notification atop the workspace. Blockly provides a + * default toast implementation, but developers may provide their own via + * setToast. For simple appearance customization, CSS should be sufficient. + * + * @param workspace The workspace to display the toast notification atop. + * @param options Configuration options for the notification, including its + * message and duration. + */ +export function toast(workspace: WorkspaceSvg, options: ToastOptions) { + toastImplementation(workspace, options); +} + +/** + * Sets the function to be run when Blockly.dialog.toast() is called. + * + * @param toastFunction The function to be run, or undefined to restore the + * default implementation. + * @see Blockly.dialog.toast + */ +export function setToast( + toastFunction: ( + workspace: WorkspaceSvg, + options: ToastOptions, + ) => void = defaultToast, +) { + toastImplementation = toastFunction; +} diff --git a/core/drag_target.ts b/packages/blockly/core/drag_target.ts similarity index 100% rename from core/drag_target.ts rename to packages/blockly/core/drag_target.ts diff --git a/core/dragging.ts b/packages/blockly/core/dragging.ts similarity index 100% rename from core/dragging.ts rename to packages/blockly/core/dragging.ts diff --git a/core/dragging/block_drag_strategy.ts b/packages/blockly/core/dragging/block_drag_strategy.ts similarity index 82% rename from core/dragging/block_drag_strategy.ts rename to packages/blockly/core/dragging/block_drag_strategy.ts index c9a1ea0abf7..0fb6d531eea 100644 --- a/core/dragging/block_drag_strategy.ts +++ b/packages/blockly/core/dragging/block_drag_strategy.ts @@ -14,8 +14,10 @@ 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 {IBubble} from '../interfaces/i_bubble.js'; import {IConnectionPreviewer} from '../interfaces/i_connection_previewer.js'; import {IDragStrategy} from '../interfaces/i_draggable.js'; +import {IHasBubble, hasBubble} from '../interfaces/i_has_bubble.js'; import * as layers from '../layers.js'; import * as registry from '../registry.js'; import {finishQueuedRenders} from '../render_management.js'; @@ -62,8 +64,8 @@ export class BlockDragStrategy implements IDragStrategy { */ private dragOffset = new Coordinate(0, 0); - /** Was there already an event group in progress when the drag started? */ - private inGroup: boolean = false; + /** Used to persist an event group when snapping is done async. */ + private originalEventGroup = ''; constructor(private block: BlockSvg) { this.workspace = block.workspace; @@ -78,7 +80,7 @@ export class BlockDragStrategy implements IDragStrategy { return ( this.block.isOwnMovable() && !this.block.isDeadOrDying() && - !this.workspace.options.readOnly && + !this.workspace.isReadOnly() && // We never drag blocks in the flyout, only create new blocks that are // dragged. !this.block.isInFlyout @@ -96,10 +98,6 @@ export class BlockDragStrategy implements IDragStrategy { } this.dragging = true; - this.inGroup = !!eventUtils.getGroup(); - if (!this.inGroup) { - eventUtils.setGroup(true); - } this.fireDragStartEvent(); this.startLoc = this.block.getRelativeToSurfaceXY(); @@ -117,13 +115,52 @@ export class BlockDragStrategy implements IDragStrategy { this.workspace.setResizesEnabled(false); blockAnimation.disconnectUiStop(); - const healStack = !!e && (e.altKey || e.ctrlKey || e.metaKey); + const healStack = this.shouldHealStack(e); if (this.shouldDisconnect(healStack)) { this.disconnectBlock(healStack); } this.block.setDragging(true); this.workspace.getLayerManager()?.moveToDragLayer(this.block); + this.getVisibleBubbles(this.block).forEach((bubble) => { + this.workspace.getLayerManager()?.moveToDragLayer(bubble, false); + }); + } + + /** + * Returns an array of visible bubbles attached to the given block or its + * descendants. + * + * @param block The block to identify open bubbles on. + * @returns An array of all currently visible bubbles on the given block or + * its descendants. + */ + private getVisibleBubbles(block: BlockSvg): IBubble[] { + return block + .getDescendants(false) + .flatMap((block) => block.getIcons()) + .filter((icon) => hasBubble(icon) && icon.bubbleIsVisible()) + .map((icon) => (icon as unknown as IHasBubble).getBubble()) + .filter((bubble) => !!bubble) // Convince TS they're non-null. + .sort((a, b) => { + // Sort the bubbles by their position in the DOM in order to maintain + // their relative z-ordering when moving between layers. + const position = a.getSvgRoot().compareDocumentPosition(b.getSvgRoot()); + if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1; + if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1; + return 0; + }); + } + + /** + * Get whether the drag should act on a single block or a block stack. + * + * @param e The instigating pointer event, if any. + * @returns True if just the initial block should be dragged out, false + * if all following blocks should also be dragged. + */ + protected shouldHealStack(e: PointerEvent | undefined) { + return !!e && (e.altKey || e.ctrlKey || e.metaKey); } /** Starts a drag on a shadow, recording the drag offset. */ @@ -231,7 +268,7 @@ export class BlockDragStrategy implements IDragStrategy { const currCandidate = this.connectionCandidate; const newCandidate = this.getConnectionCandidate(draggingBlock, delta); if (!newCandidate) { - this.connectionPreviewer!.hidePreview(); + this.connectionPreviewer?.hidePreview(); this.connectionCandidate = null; return; } @@ -247,7 +284,7 @@ export class BlockDragStrategy implements IDragStrategy { local.type === ConnectionType.OUTPUT_VALUE || local.type === ConnectionType.PREVIOUS_STATEMENT; const neighbourIsConnectedToRealBlock = - neighbour.isConnected() && !neighbour.targetBlock()!.isInsertionMarker(); + neighbour.isConnected() && !neighbour.targetBlock()?.isInsertionMarker(); if ( localIsOutputOrPrevious && neighbourIsConnectedToRealBlock && @@ -257,14 +294,14 @@ export class BlockDragStrategy implements IDragStrategy { local.type, ) ) { - this.connectionPreviewer!.previewReplacement( + this.connectionPreviewer?.previewReplacement( local, neighbour, neighbour.targetBlock()!, ); return; } - this.connectionPreviewer!.previewConnection(local, neighbour); + this.connectionPreviewer?.previewConnection(local, neighbour); } /** @@ -319,9 +356,7 @@ export class BlockDragStrategy implements IDragStrategy { delta: Coordinate, ): ConnectionCandidate | null { const localConns = this.getLocalConnections(draggingBlock); - let radius = this.connectionCandidate - ? config.connectingSnapRadius - : config.snapRadius; + let radius = this.getSearchRadius(); let candidate = null; for (const conn of localConns) { @@ -339,6 +374,15 @@ export class BlockDragStrategy implements IDragStrategy { return candidate; } + /** + * Get the radius to use when searching for a nearby valid connection. + */ + protected getSearchRadius() { + return this.connectionCandidate + ? config.connectingSnapRadius + : config.snapRadius; + } + /** * Returns all of the connections we might connect to blocks on the workspace. * @@ -363,6 +407,7 @@ export class BlockDragStrategy implements IDragStrategy { this.block.getParent()?.endDrag(e); return; } + this.originalEventGroup = eventUtils.getGroup(); this.fireDragEndEvent(); this.fireMoveEvent(); @@ -370,7 +415,7 @@ export class BlockDragStrategy implements IDragStrategy { dom.stopTextWidthCache(); blockAnimation.disconnectUiStop(); - this.connectionPreviewer!.hidePreview(); + this.connectionPreviewer?.hidePreview(); if (!this.block.isDeadOrDying() && this.dragging) { // These are expensive and don't need to be done if we're deleting, or @@ -378,6 +423,13 @@ export class BlockDragStrategy implements IDragStrategy { this.workspace .getLayerManager() ?.moveOffDragLayer(this.block, layers.BLOCK); + + this.getVisibleBubbles(this.block).forEach((bubble) => + this.workspace + .getLayerManager() + ?.moveOffDragLayer(bubble, layers.BUBBLE, false), + ); + this.block.setDragging(false); } @@ -388,20 +440,19 @@ export class BlockDragStrategy implements IDragStrategy { } 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() { + const newGroup = eventUtils.getGroup(); + eventUtils.setGroup(this.originalEventGroup); this.block.snapToGrid(); // Must dispose after connections are applied to not break the dynamic // connections plugin. See #7859 - this.connectionPreviewer!.dispose(); + this.connectionPreviewer?.dispose(); this.workspace.setResizesEnabled(true); + eventUtils.setGroup(newGroup); } /** Connects the given candidate connections. */ @@ -431,6 +482,9 @@ export class BlockDragStrategy implements IDragStrategy { return; } + this.connectionPreviewer?.hidePreview(); + this.connectionCandidate = null; + this.startChildConn?.connect(this.block.nextConnection); if (this.startParentConn) { switch (this.startParentConn.type) { @@ -445,6 +499,12 @@ export class BlockDragStrategy implements IDragStrategy { this.workspace .getLayerManager() ?.moveOffDragLayer(this.block, layers.BLOCK); + this.getVisibleBubbles(this.block).forEach((bubble) => + this.workspace + .getLayerManager() + ?.moveOffDragLayer(bubble, layers.BUBBLE, false), + ); + // Blocks dragged directly from a flyout may need to be bumped into // bounds. bumpObjects.bumpIntoBounds( @@ -457,9 +517,6 @@ export class BlockDragStrategy implements IDragStrategy { 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/packages/blockly/core/dragging/bubble_drag_strategy.ts similarity index 77% rename from core/dragging/bubble_drag_strategy.ts rename to packages/blockly/core/dragging/bubble_drag_strategy.ts index c2a5c58f4a2..8a5a6783910 100644 --- a/core/dragging/bubble_drag_strategy.ts +++ b/packages/blockly/core/dragging/bubble_drag_strategy.ts @@ -5,7 +5,6 @@ */ 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'; @@ -13,9 +12,6 @@ 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, @@ -26,10 +22,6 @@ export class BubbleDragStrategy implements IDragStrategy { } 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); @@ -44,9 +36,6 @@ export class BubbleDragStrategy implements IDragStrategy { endDrag(): void { this.workspace.setResizesEnabled(true); - if (!this.inGroup) { - eventUtils.setGroup(false); - } this.workspace .getLayerManager() diff --git a/core/dragging/comment_drag_strategy.ts b/packages/blockly/core/dragging/comment_drag_strategy.ts similarity index 88% rename from core/dragging/comment_drag_strategy.ts rename to packages/blockly/core/dragging/comment_drag_strategy.ts index dd8b10fc2f9..b7974d8b4ca 100644 --- a/core/dragging/comment_drag_strategy.ts +++ b/packages/blockly/core/dragging/comment_drag_strategy.ts @@ -18,9 +18,6 @@ export class CommentDragStrategy implements IDragStrategy { 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; } @@ -29,15 +26,11 @@ export class CommentDragStrategy implements IDragStrategy { return ( this.comment.isOwnMovable() && !this.comment.isDeadOrDying() && - !this.workspace.options.readOnly + !this.workspace.isReadOnly() ); } startDrag(): void { - this.inGroup = !!eventUtils.getGroup(); - if (!this.inGroup) { - eventUtils.setGroup(true); - } this.fireDragStartEvent(); this.startLoc = this.comment.getRelativeToSurfaceXY(); this.workspace.setResizesEnabled(false); @@ -61,9 +54,6 @@ export class CommentDragStrategy implements IDragStrategy { this.comment.snapToGrid(); this.workspace.setResizesEnabled(true); - if (!this.inGroup) { - eventUtils.setGroup(false); - } } /** Fire a UI event at the start of a comment drag. */ diff --git a/core/dragging/dragger.ts b/packages/blockly/core/dragging/dragger.ts similarity index 89% rename from core/dragging/dragger.ts rename to packages/blockly/core/dragging/dragger.ts index 8a9ac87c6a9..02e9e2bfb79 100644 --- a/core/dragging/dragger.ts +++ b/packages/blockly/core/dragging/dragger.ts @@ -8,11 +8,13 @@ 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 {getFocusManager} from '../focus_manager.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 {isFocusableNode} from '../interfaces/i_focusable_node.js'; import * as registry from '../registry.js'; import {Coordinate} from '../utils/coordinate.js'; import {WorkspaceSvg} from '../workspace_svg.js'; @@ -31,6 +33,9 @@ export class Dragger implements IDragger { /** Handles any drag startup. */ onDragStart(e: PointerEvent) { + if (!eventUtils.getGroup()) { + eventUtils.setGroup(true); + } this.draggable.startDrag(e); } @@ -119,12 +124,18 @@ export class Dragger implements IDragger { 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(); + // We want to make sure the delete gets grouped with any possible move + // event. In core Blockly this shouldn't happen, but due to a change + // in behavior older custom draggables might still clear the group. eventUtils.setGroup(origGroup); root.dispose(); - eventUtils.setGroup(newGroup); + } + eventUtils.setGroup(false); + + if (!wouldDelete && isFocusableNode(this.draggable)) { + // Ensure focusable nodes that have finished dragging (but aren't being + // deleted) end with focus and selection. + getFocusManager().focusNode(this.draggable); } } diff --git a/core/dropdowndiv.ts b/packages/blockly/core/dropdowndiv.ts similarity index 88% rename from core/dropdowndiv.ts rename to packages/blockly/core/dropdowndiv.ts index f9af02ac9f7..ceab467a895 100644 --- a/core/dropdowndiv.ts +++ b/packages/blockly/core/dropdowndiv.ts @@ -13,8 +13,10 @@ // Former goog.module ID: Blockly.dropDownDiv import type {BlockSvg} from './block_svg.js'; +import * as browserEvents from './browser_events.js'; import * as common from './common.js'; import type {Field} from './field.js'; +import {ReturnEphemeralFocus, getFocusManager} from './focus_manager.js'; import * as dom from './utils/dom.js'; import * as math from './utils/math.js'; import {Rect} from './utils/rect.js'; @@ -82,6 +84,12 @@ let owner: Field | null = null; /** Whether the dropdown was positioned to a field or the source block. */ let positionToField: boolean | null = null; +/** Callback to FocusManager to return ephemeral focus when the div closes. */ +let returnEphemeralFocus: ReturnEphemeralFocus | null = null; + +/** Identifier for shortcut keydown listener used to unbind it. */ +let keydownListener: browserEvents.Data | null = null; + /** * Dropdown bounds info object used to encapsulate sizing information about a * bounding element (bounding box and width/height). @@ -118,6 +126,7 @@ export function createDom() { } div = document.createElement('div'); div.className = 'blocklyDropDownDiv'; + div.tabIndex = -1; const parentDiv = common.getParentContainer() || document.body; parentDiv.appendChild(div); @@ -125,6 +134,13 @@ export function createDom() { content.className = 'blocklyDropDownContent'; div.appendChild(content); + keydownListener = browserEvents.conditionalBind( + content, + 'keydown', + null, + common.globalShortcutHandler, + ); + arrow = document.createElement('div'); arrow.className = 'blocklyDropDownArrow'; div.appendChild(arrow); @@ -133,15 +149,6 @@ export function createDom() { // 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'); - }); } /** @@ -166,14 +173,18 @@ export function getOwner(): Field | null { * * @returns Div to populate with content. */ -export function getContentDiv(): Element { +export function getContentDiv(): HTMLDivElement { return content; } /** Clear the content of the drop-down. */ export function clearContent() { - content.textContent = ''; - content.style.width = ''; + if (keydownListener) { + browserEvents.unbind(keydownListener); + keydownListener = null; + } + div.remove(); + createDom(); } /** @@ -197,6 +208,11 @@ export function setColour(backgroundColour: string, borderColour: string) { * @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. + * @param manageEphemeralFocus Whether ephemeral focus should be managed + * according to the drop-down div's lifetime. Note that if a false value is + * passed in here then callers should manage ephemeral focus directly + * otherwise focus may not properly restore when the widget closes. Defaults + * to true. * @returns True if the menu rendered below block; false if above. */ export function showPositionedByBlock( @@ -204,10 +220,12 @@ export function showPositionedByBlock( block: BlockSvg, opt_onHide?: () => void, opt_secondaryYOffset?: number, + manageEphemeralFocus: boolean = true, ): boolean { return showPositionedByRect( getScaledBboxOfBlock(block), field as Field, + manageEphemeralFocus, opt_onHide, opt_secondaryYOffset, ); @@ -222,17 +240,24 @@ export function showPositionedByBlock( * @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. + * @param manageEphemeralFocus Whether ephemeral focus should be managed + * according to the drop-down div's lifetime. Note that if a false value is + * passed in here then callers should manage ephemeral focus directly + * otherwise focus may not properly restore when the widget closes. Defaults + * to true. * @returns True if the menu rendered below block; false if above. */ export function showPositionedByField( field: Field, opt_onHide?: () => void, opt_secondaryYOffset?: number, + manageEphemeralFocus: boolean = true, ): boolean { positionToField = true; return showPositionedByRect( getScaledBboxOfField(field as Field), field as Field, + manageEphemeralFocus, opt_onHide, opt_secondaryYOffset, ); @@ -273,11 +298,16 @@ function getScaledBboxOfField(field: Field): Rect { * @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. + * @param manageEphemeralFocus Whether ephemeral focus should be managed + * according to the drop-down div's lifetime. Note that if a false value is + * passed in here then callers should manage ephemeral focus directly + * otherwise focus may not properly restore when the widget closes. * @returns True if the menu rendered below block; false if above. */ function showPositionedByRect( bBox: Rect, field: Field, + manageEphemeralFocus: boolean, opt_onHide?: () => void, opt_secondaryYOffset?: number, ): boolean { @@ -304,6 +334,7 @@ function showPositionedByRect( primaryY, secondaryX, secondaryY, + manageEphemeralFocus, opt_onHide, ); } @@ -324,6 +355,8 @@ function showPositionedByRect( * @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. + * @param manageEphemeralFocus Whether ephemeral focus should be managed + * according to the widget div's lifetime. * @returns True if the menu rendered at the primary origin point. * @internal */ @@ -334,6 +367,7 @@ export function show( primaryY: number, secondaryX: number, secondaryY: number, + manageEphemeralFocus: boolean, opt_onHide?: () => void, ): boolean { owner = newOwner as Field; @@ -344,12 +378,8 @@ export function show( 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); - } + dom.addClass(div, renderedClassName); + dom.addClass(div, themeClassName); // When we change `translate` multiple times in close succession, // Chrome may choose to wait and apply them all at once. @@ -359,7 +389,15 @@ export function show( // 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 atOrigin = positionInternal(primaryX, primaryY, secondaryX, secondaryY); + + // Ephemeral focus must happen after the div is fully visible in order to + // ensure that it properly receives focus. + if (manageEphemeralFocus) { + returnEphemeralFocus = getFocusManager().takeEphemeralFocus(div); + } + + return atOrigin; } const internal = { @@ -651,16 +689,6 @@ export function hideWithoutAnimation() { 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; @@ -668,15 +696,12 @@ export function hideWithoutAnimation() { clearContent(); owner = null; - if (renderedClassName) { - dom.removeClass(div, renderedClassName); - renderedClassName = ''; - } - if (themeClassName) { - dom.removeClass(div, themeClassName); - themeClassName = ''; - } (common.getMainWorkspace() as WorkspaceSvg).markFocused(); + + if (returnEphemeralFocus) { + returnEphemeralFocus(); + returnEphemeralFocus = null; + } } /** @@ -703,19 +728,12 @@ function positionInternal( // Update arrow CSS. if (metrics.arrowVisible) { + const x = metrics.arrowX; + const y = metrics.arrowY; + const rotation = metrics.arrowAtTop ? 45 : 225; arrow.style.display = ''; - arrow.style.transform = - 'translate(' + - metrics.arrowX + - 'px,' + - metrics.arrowY + - 'px) rotate(45deg)'; - arrow.setAttribute( - 'class', - metrics.arrowAtTop - ? 'blocklyDropDownArrow blocklyArrowTop' - : 'blocklyDropDownArrow blocklyArrowBottom', - ); + arrow.style.transform = `translate(${x}px, ${y}px) rotate(${rotation}deg)`; + arrow.setAttribute('class', 'blocklyDropDownArrow'); } else { arrow.style.display = 'none'; } diff --git a/core/events/events.ts b/packages/blockly/core/events/events.ts similarity index 97% rename from core/events/events.ts rename to packages/blockly/core/events/events.ts index 86899565381..dcddf19a925 100644 --- a/core/events/events.ts +++ b/packages/blockly/core/events/events.ts @@ -33,19 +33,21 @@ 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'; + +// Events. 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 {VarTypeChange, VarTypeChangeJson} from './events_var_type_change.js'; export {ViewportChange, ViewportChangeJson} from './events_viewport.js'; export {FinishedLoading} from './workspace_events.js'; @@ -74,7 +76,6 @@ 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; diff --git a/core/events/events_abstract.ts b/packages/blockly/core/events/events_abstract.ts similarity index 100% rename from core/events/events_abstract.ts rename to packages/blockly/core/events/events_abstract.ts diff --git a/core/events/events_block_base.ts b/packages/blockly/core/events/events_block_base.ts similarity index 100% rename from core/events/events_block_base.ts rename to packages/blockly/core/events/events_block_base.ts diff --git a/core/events/events_block_change.ts b/packages/blockly/core/events/events_block_change.ts similarity index 99% rename from core/events/events_block_change.ts rename to packages/blockly/core/events/events_block_change.ts index e71eabb1747..d4a8ba9d2e6 100644 --- a/core/events/events_block_change.ts +++ b/packages/blockly/core/events/events_block_change.ts @@ -193,7 +193,7 @@ export class BlockChange extends BlockBase { break; } case 'comment': - block.setCommentText((value as string) || null); + block.setCommentText((value as string) ?? null); break; case 'collapsed': block.setCollapsed(!!value); diff --git a/core/events/events_block_create.ts b/packages/blockly/core/events/events_block_create.ts similarity index 100% rename from core/events/events_block_create.ts rename to packages/blockly/core/events/events_block_create.ts diff --git a/core/events/events_block_delete.ts b/packages/blockly/core/events/events_block_delete.ts similarity index 100% rename from core/events/events_block_delete.ts rename to packages/blockly/core/events/events_block_delete.ts diff --git a/core/events/events_block_drag.ts b/packages/blockly/core/events/events_block_drag.ts similarity index 100% rename from core/events/events_block_drag.ts rename to packages/blockly/core/events/events_block_drag.ts diff --git a/core/events/events_block_field_intermediate_change.ts b/packages/blockly/core/events/events_block_field_intermediate_change.ts similarity index 100% rename from core/events/events_block_field_intermediate_change.ts rename to packages/blockly/core/events/events_block_field_intermediate_change.ts diff --git a/core/events/events_block_move.ts b/packages/blockly/core/events/events_block_move.ts similarity index 100% rename from core/events/events_block_move.ts rename to packages/blockly/core/events/events_block_move.ts diff --git a/core/events/events_bubble_open.ts b/packages/blockly/core/events/events_bubble_open.ts similarity index 100% rename from core/events/events_bubble_open.ts rename to packages/blockly/core/events/events_bubble_open.ts diff --git a/core/events/events_click.ts b/packages/blockly/core/events/events_click.ts similarity index 100% rename from core/events/events_click.ts rename to packages/blockly/core/events/events_click.ts diff --git a/core/events/events_comment_base.ts b/packages/blockly/core/events/events_comment_base.ts similarity index 100% rename from core/events/events_comment_base.ts rename to packages/blockly/core/events/events_comment_base.ts diff --git a/core/events/events_comment_change.ts b/packages/blockly/core/events/events_comment_change.ts similarity index 100% rename from core/events/events_comment_change.ts rename to packages/blockly/core/events/events_comment_change.ts diff --git a/core/events/events_comment_collapse.ts b/packages/blockly/core/events/events_comment_collapse.ts similarity index 100% rename from core/events/events_comment_collapse.ts rename to packages/blockly/core/events/events_comment_collapse.ts diff --git a/core/events/events_comment_create.ts b/packages/blockly/core/events/events_comment_create.ts similarity index 100% rename from core/events/events_comment_create.ts rename to packages/blockly/core/events/events_comment_create.ts diff --git a/core/events/events_comment_delete.ts b/packages/blockly/core/events/events_comment_delete.ts similarity index 100% rename from core/events/events_comment_delete.ts rename to packages/blockly/core/events/events_comment_delete.ts diff --git a/core/events/events_comment_drag.ts b/packages/blockly/core/events/events_comment_drag.ts similarity index 100% rename from core/events/events_comment_drag.ts rename to packages/blockly/core/events/events_comment_drag.ts diff --git a/core/events/events_comment_move.ts b/packages/blockly/core/events/events_comment_move.ts similarity index 100% rename from core/events/events_comment_move.ts rename to packages/blockly/core/events/events_comment_move.ts diff --git a/core/events/events_comment_resize.ts b/packages/blockly/core/events/events_comment_resize.ts similarity index 100% rename from core/events/events_comment_resize.ts rename to packages/blockly/core/events/events_comment_resize.ts diff --git a/core/events/events_selected.ts b/packages/blockly/core/events/events_selected.ts similarity index 100% rename from core/events/events_selected.ts rename to packages/blockly/core/events/events_selected.ts diff --git a/core/events/events_theme_change.ts b/packages/blockly/core/events/events_theme_change.ts similarity index 100% rename from core/events/events_theme_change.ts rename to packages/blockly/core/events/events_theme_change.ts diff --git a/core/events/events_toolbox_item_select.ts b/packages/blockly/core/events/events_toolbox_item_select.ts similarity index 100% rename from core/events/events_toolbox_item_select.ts rename to packages/blockly/core/events/events_toolbox_item_select.ts diff --git a/core/events/events_trashcan_open.ts b/packages/blockly/core/events/events_trashcan_open.ts similarity index 100% rename from core/events/events_trashcan_open.ts rename to packages/blockly/core/events/events_trashcan_open.ts diff --git a/core/events/events_ui_base.ts b/packages/blockly/core/events/events_ui_base.ts similarity index 100% rename from core/events/events_ui_base.ts rename to packages/blockly/core/events/events_ui_base.ts diff --git a/core/events/events_var_base.ts b/packages/blockly/core/events/events_var_base.ts similarity index 89% rename from core/events/events_var_base.ts rename to packages/blockly/core/events/events_var_base.ts index 8e359de517f..f128f67b410 100644 --- a/core/events/events_var_base.ts +++ b/packages/blockly/core/events/events_var_base.ts @@ -11,7 +11,10 @@ */ // Former goog.module ID: Blockly.Events.VarBase -import type {VariableModel} from '../variable_model.js'; +import type { + IVariableModel, + IVariableState, +} from '../interfaces/i_variable_model.js'; import type {Workspace} from '../workspace.js'; import { Abstract as AbstractEvent, @@ -30,13 +33,13 @@ export class VarBase extends AbstractEvent { * @param opt_variable The variable this event corresponds to. Undefined for * a blank event. */ - constructor(opt_variable?: VariableModel) { + constructor(opt_variable?: IVariableModel) { super(); this.isBlank = typeof opt_variable === 'undefined'; if (!opt_variable) return; this.varId = opt_variable.getId(); - this.workspaceId = opt_variable.workspace.id; + this.workspaceId = opt_variable.getWorkspace().id; } /** diff --git a/core/events/events_var_create.ts b/packages/blockly/core/events/events_var_create.ts similarity index 85% rename from core/events/events_var_create.ts rename to packages/blockly/core/events/events_var_create.ts index b3ae548aa0d..c34c7ff57ae 100644 --- a/core/events/events_var_create.ts +++ b/packages/blockly/core/events/events_var_create.ts @@ -11,8 +11,12 @@ */ // Former goog.module ID: Blockly.Events.VarCreate +import type { + IVariableModel, + IVariableState, +} from '../interfaces/i_variable_model.js'; 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'; @@ -32,14 +36,14 @@ export class VarCreate extends VarBase { /** * @param opt_variable The created variable. Undefined for a blank event. */ - constructor(opt_variable?: VariableModel) { + constructor(opt_variable?: IVariableModel) { super(opt_variable); if (!opt_variable) { return; // Blank event to be populated by fromJson. } - this.varType = opt_variable.type; - this.varName = opt_variable.name; + this.varType = opt_variable.getType(); + this.varName = opt_variable.getName(); } /** @@ -109,10 +113,12 @@ export class VarCreate extends VarBase { 'the constructor, or call fromJson', ); } + const variableMap = workspace.getVariableMap(); if (forward) { - workspace.createVariable(this.varName, this.varType, this.varId); + variableMap.createVariable(this.varName, this.varType, this.varId); } else { - workspace.deleteVariableById(this.varId); + const variable = variableMap.getVariableById(this.varId); + if (variable) variableMap.deleteVariable(variable); } } } diff --git a/core/events/events_var_delete.ts b/packages/blockly/core/events/events_var_delete.ts similarity index 85% rename from core/events/events_var_delete.ts rename to packages/blockly/core/events/events_var_delete.ts index caaa1f4874a..62317e36c50 100644 --- a/core/events/events_var_delete.ts +++ b/packages/blockly/core/events/events_var_delete.ts @@ -6,16 +6,18 @@ // Former goog.module ID: Blockly.Events.VarDelete +import type { + IVariableModel, + IVariableState, +} from '../interfaces/i_variable_model.js'; 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; @@ -27,14 +29,14 @@ export class VarDelete extends VarBase { /** * @param opt_variable The deleted variable. Undefined for a blank event. */ - constructor(opt_variable?: VariableModel) { + constructor(opt_variable?: IVariableModel) { super(opt_variable); if (!opt_variable) { return; // Blank event to be populated by fromJson. } - this.varType = opt_variable.type; - this.varName = opt_variable.name; + this.varType = opt_variable.getType(); + this.varName = opt_variable.getName(); } /** @@ -104,10 +106,12 @@ export class VarDelete extends VarBase { 'the constructor, or call fromJson', ); } + const variableMap = workspace.getVariableMap(); if (forward) { - workspace.deleteVariableById(this.varId); + const variable = variableMap.getVariableById(this.varId); + if (variable) variableMap.deleteVariable(variable); } else { - workspace.createVariable(this.varName, this.varType, this.varId); + variableMap.createVariable(this.varName, this.varType, this.varId); } } } diff --git a/core/events/events_var_rename.ts b/packages/blockly/core/events/events_var_rename.ts similarity index 86% rename from core/events/events_var_rename.ts rename to packages/blockly/core/events/events_var_rename.ts index b461184cab1..a1758738c22 100644 --- a/core/events/events_var_rename.ts +++ b/packages/blockly/core/events/events_var_rename.ts @@ -6,16 +6,18 @@ // Former goog.module ID: Blockly.Events.VarRename +import type { + IVariableModel, + IVariableState, +} from '../interfaces/i_variable_model.js'; 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; @@ -30,13 +32,13 @@ export class VarRename extends VarBase { * @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) { + constructor(opt_variable?: IVariableModel, newName?: string) { super(opt_variable); if (!opt_variable) { return; // Blank event to be populated by fromJson. } - this.oldName = opt_variable.name; + this.oldName = opt_variable.getName(); this.newName = typeof newName === 'undefined' ? '' : newName; } @@ -113,10 +115,12 @@ export class VarRename extends VarBase { 'the constructor, or call fromJson', ); } + const variableMap = workspace.getVariableMap(); + const variable = variableMap.getVariableById(this.varId); if (forward) { - workspace.renameVariableById(this.varId, this.newName); + if (variable) variableMap.renameVariable(variable, this.newName); } else { - workspace.renameVariableById(this.varId, this.oldName); + if (variable) variableMap.renameVariable(variable, this.oldName); } } } diff --git a/packages/blockly/core/events/events_var_type_change.ts b/packages/blockly/core/events/events_var_type_change.ts new file mode 100644 index 00000000000..c02a7e45435 --- /dev/null +++ b/packages/blockly/core/events/events_var_type_change.ts @@ -0,0 +1,122 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Class for a variable type change event. + * + * @class + */ + +import type { + IVariableModel, + IVariableState, +} from '../interfaces/i_variable_model.js'; +import * as registry from '../registry.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's type has changed. + */ +export class VarTypeChange extends VarBase { + override type = EventType.VAR_TYPE_CHANGE; + + /** + * @param variable The variable whose type changed. Undefined for a blank event. + * @param oldType The old type of the variable. Undefined for a blank event. + * @param newType The new type of the variable. Undefined for a blank event. + */ + constructor( + variable?: IVariableModel, + public oldType?: string, + public newType?: string, + ) { + super(variable); + } + + /** + * Encode the event as JSON. + * + * @returns JSON representation. + */ + override toJson(): VarTypeChangeJson { + const json = super.toJson() as VarTypeChangeJson; + if (!this.oldType || !this.newType) { + throw new Error( + "The variable's types are undefined. Either pass them to " + + 'the constructor, or call fromJson', + ); + } + json['oldType'] = this.oldType; + json['newType'] = this.newType; + return json; + } + + /** + * Deserializes the JSON event. + * + * @param event The event to append new properties to. Should be a subclass + * of VarTypeChange, 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: VarTypeChangeJson, + workspace: Workspace, + event?: any, + ): VarTypeChange { + const newEvent = super.fromJson( + json, + workspace, + event ?? new VarTypeChange(), + ) as VarTypeChange; + newEvent.oldType = json['oldType']; + newEvent.newType = json['newType']; + return newEvent; + } + + /** + * Run a variable type change 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.oldType || !this.newType) { + throw new Error( + "The variable's types are undefined. Either pass them to " + + 'the constructor, or call fromJson', + ); + } + const variable = workspace.getVariableMap().getVariableById(this.varId); + if (!variable) return; + if (forward) { + workspace.getVariableMap().changeVariableType(variable, this.newType); + } else { + workspace.getVariableMap().changeVariableType(variable, this.oldType); + } + } +} + +export interface VarTypeChangeJson extends VarBaseJson { + oldType: string; + newType: string; +} + +registry.register( + registry.Type.EVENT, + EventType.VAR_TYPE_CHANGE, + VarTypeChange, +); diff --git a/core/events/events_viewport.ts b/packages/blockly/core/events/events_viewport.ts similarity index 100% rename from core/events/events_viewport.ts rename to packages/blockly/core/events/events_viewport.ts diff --git a/core/events/predicates.ts b/packages/blockly/core/events/predicates.ts similarity index 96% rename from core/events/predicates.ts rename to packages/blockly/core/events/predicates.ts index 79d8ca284e4..9e8ce3b3a59 100644 --- a/core/events/predicates.ts +++ b/packages/blockly/core/events/predicates.ts @@ -29,7 +29,6 @@ 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'; @@ -99,11 +98,6 @@ 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; diff --git a/core/events/type.ts b/packages/blockly/core/events/type.ts similarity index 96% rename from core/events/type.ts rename to packages/blockly/core/events/type.ts index db9ad6c96a3..0928b8ff077 100644 --- a/core/events/type.ts +++ b/packages/blockly/core/events/type.ts @@ -28,6 +28,8 @@ export enum EventType { VAR_DELETE = 'var_delete', /** Type of event that renames a variable. */ VAR_RENAME = 'var_rename', + /** Type of event that changes the type of a variable. */ + VAR_TYPE_CHANGE = 'var_type_change', /** * Type of generic event that records a UI change. * diff --git a/core/events/utils.ts b/packages/blockly/core/events/utils.ts similarity index 95% rename from core/events/utils.ts rename to packages/blockly/core/events/utils.ts index 4753e7783d9..ac78c694273 100644 --- a/core/events/utils.ts +++ b/packages/blockly/core/events/utils.ts @@ -9,7 +9,6 @@ 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'; @@ -124,7 +123,7 @@ function fireInternal(event: Abstract) { /** Dispatch all queued events. */ function fireNow() { - const queue = filter(FIRE_QUEUE, true); + const queue = filter(FIRE_QUEUE); FIRE_QUEUE.length = 0; for (const event of queue) { if (!event.workspaceId) continue; @@ -227,18 +226,9 @@ function enqueueEvent(event: Abstract) { * 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. - } +export function filter(queue: Abstract[]): Abstract[] { const mergedQueue: Abstract[] = []; // Merge duplicates. for (const event of queue) { @@ -290,10 +280,6 @@ export function filter(queue: Abstract[], forward = true): Abstract[] { } // 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; } diff --git a/core/events/workspace_events.ts b/packages/blockly/core/events/workspace_events.ts similarity index 100% rename from core/events/workspace_events.ts rename to packages/blockly/core/events/workspace_events.ts diff --git a/core/extensions.ts b/packages/blockly/core/extensions.ts similarity index 99% rename from core/extensions.ts rename to packages/blockly/core/extensions.ts index 0957b7f86ca..59d218d17fa 100644 --- a/core/extensions.ts +++ b/packages/blockly/core/extensions.ts @@ -437,7 +437,10 @@ function checkDropdownOptionsInTable( } const options = dropdown.getOptions(); - for (const [, key] of options) { + for (const option of options) { + if (option === FieldDropdown.SEPARATOR) continue; + + const [, key] = option; if (lookupTable[key] === undefined) { console.warn( `No tooltip mapping for value ${key} of field ` + diff --git a/core/field.ts b/packages/blockly/core/field.ts similarity index 91% rename from core/field.ts rename to packages/blockly/core/field.ts index 4c4b90cf55a..2d25eb9dc83 100644 --- a/core/field.ts +++ b/packages/blockly/core/field.ts @@ -23,17 +23,17 @@ 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 {IFocusableNode} from './interfaces/i_focusable_node.js'; +import type {IFocusableTree} from './interfaces/i_focusable_tree.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 idGenerator from './utils/idgenerator.js'; import * as parsing from './utils/parsing.js'; import {Rect} from './utils/rect.js'; import {Size} from './utils/size.js'; @@ -42,7 +42,7 @@ 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'; +import {WorkspaceSvg} from './workspace_svg.js'; /** * A function that is called to validate changes to the field's value before @@ -67,12 +67,7 @@ export type FieldValidator = (newValue: T) => T | null | undefined; * @typeParam T - The value stored on the field. */ export abstract class Field - implements - IASTNodeLocationSvg, - IASTNodeLocationWithBlock, - IKeyboardAccessible, - IRegistrable, - ISerializable + implements IKeyboardAccessible, IRegistrable, ISerializable, IFocusableNode { /** * To overwrite the default value which is set in **Field**, directly update @@ -108,19 +103,25 @@ export abstract class Field * 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; + /** This field's dimensions. */ + private size: Size = new Size(0, 0); /** - * Holds the markers svg element when the marker is attached to the field. - * This is null if there is no marker on the field. + * Gets the size of this field. Because getSize() and updateSize() have side + * effects, this acts as a shim for subclasses which wish to adjust field + * bounds when setting/getting the size without triggering unwanted rendering + * or other side effects. Note that subclasses must override *both* get and + * set if either is overridden; the implementation may just call directly + * through to super, but it must exist per the JS spec. */ - private markerSvg: SVGElement | null = null; + protected get size_(): Size { + return this.size; + } + + protected set size_(newValue: Size) { + this.size = newValue; + } /** The rendered field's SVG group element. */ protected fieldGroup_: SVGGElement | null = null; @@ -194,8 +195,8 @@ export abstract class Field */ SERIALIZABLE = false; - /** Mouse cursor style when over the hotspot that initiates the editor. */ - CURSOR = ''; + /** The unique ID of this field. */ + private id_: string | null = null; /** * @param value The initial value of the field. @@ -261,6 +262,13 @@ export abstract class Field throw Error('Field already bound to a block'); } this.sourceBlock_ = block; + if (block.id.includes('_field')) { + throw new Error( + `Field ID indicator is contained in block ID. This may cause ` + + `problems with focus: ${block.id}.`, + ); + } + this.id_ = `${block.id}_field_${idGenerator.getNextUniqueId()}`; } /** @@ -304,7 +312,11 @@ export abstract class Field // Field has already been initialized once. return; } - this.fieldGroup_ = dom.createSvgElement(Svg.G, {}); + const id = this.id_; + if (!id) throw new Error('Expected ID to be defined prior to init.'); + this.fieldGroup_ = dom.createSvgElement(Svg.G, { + 'id': id, + }); if (!this.isVisible()) { this.fieldGroup_.style.display = 'none'; } @@ -324,6 +336,9 @@ export abstract class Field protected initView() { this.createBorderRect_(); this.createTextElement_(); + if (this.fieldGroup_) { + dom.addClass(this.fieldGroup_, 'blocklyField'); + } } /** @@ -339,8 +354,10 @@ export abstract class Field * 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. + * + * @internal */ - protected isFullBlockField(): boolean { + isFullBlockField(): boolean { return !this.borderRect_; } @@ -374,7 +391,7 @@ export abstract class Field this.textElement_ = dom.createSvgElement( Svg.TEXT, { - 'class': 'blocklyText', + 'class': 'blocklyText blocklyFieldText', }, this.fieldGroup_, ); @@ -406,7 +423,6 @@ export abstract class Field * 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! @@ -419,7 +435,6 @@ export abstract class Field * @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! @@ -438,7 +453,6 @@ export abstract class Field * {@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); @@ -453,7 +467,6 @@ export abstract class Field * 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)) { @@ -516,8 +529,6 @@ export abstract class Field /** * Dispose of all DOM objects and events belonging to this editable field. - * - * @internal */ dispose() { dropDownDiv.hideIfOwner(this); @@ -538,13 +549,11 @@ export abstract class Field return; } if (this.enabled_ && block.isEditable()) { - dom.addClass(group, 'blocklyEditableText'); - dom.removeClass(group, 'blocklyNonEditableText'); - group.style.cursor = this.CURSOR; + dom.addClass(group, 'blocklyEditableField'); + dom.removeClass(group, 'blocklyNonEditableField'); } else { - dom.addClass(group, 'blocklyNonEditableText'); - dom.removeClass(group, 'blocklyEditableText'); - group.style.cursor = ''; + dom.addClass(group, 'blocklyNonEditableField'); + dom.removeClass(group, 'blocklyEditableField'); } } @@ -833,20 +842,23 @@ export abstract class Field let contentWidth = 0; if (this.textElement_) { - contentWidth = dom.getFastTextWidth( - this.textElement_, - constants!.FIELD_TEXT_FONTSIZE, - constants!.FIELD_TEXT_FONTWEIGHT, - constants!.FIELD_TEXT_FONTFAMILY, - ); + if (this.textElement_.isConnected) { + contentWidth = dom.getTextWidth(this.textElement_); + } else { + 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.size_ = new Size(totalWidth, totalHeight); this.positionTextElement_(xOffset, contentWidth); this.positionBorderRect_(); @@ -918,17 +930,6 @@ export abstract class Field 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_; } @@ -992,10 +993,6 @@ export abstract class Field */ 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) + '…'; @@ -1057,8 +1054,6 @@ export abstract class Field * 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; @@ -1317,7 +1312,6 @@ export abstract class Field * Subclasses may override this. * * @returns True if this field has any variable references. - * @internal */ referencesVariables(): boolean { return false; @@ -1326,8 +1320,6 @@ export abstract class Field /** * Refresh the variable name referenced by this field if this field references * variables. - * - * @internal */ refreshVariableName() {} // NOP @@ -1369,15 +1361,6 @@ export abstract class Field 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. * @@ -1388,62 +1371,37 @@ export abstract class Field 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; - } - + /** See IFocusableNode.getFocusableElement. */ + getFocusableElement(): HTMLElement | SVGElement { if (!this.fieldGroup_) { - throw new Error(`The field group is ${this.fieldGroup_}.`); + throw Error('This field currently has no representative DOM element.'); } - this.fieldGroup_.appendChild(markerSvg); - this.markerSvg = markerSvg; + return this.fieldGroup_; } - /** - * Redraw any attached marker or cursor svgs if needed. - * - * @internal - */ - updateMarkers_() { + /** See IFocusableNode.getFocusableTree. */ + getFocusableTree(): IFocusableTree { 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(); - } + return block.workspace as WorkspaceSvg; + } + + /** See IFocusableNode.onNodeFocus. */ + onNodeFocus(): void { + const block = this.getSourceBlock() as BlockSvg; + block.workspace.scrollBoundsIntoView( + block.getBoundingRectangleWithoutChildren(), + ); + } + + /** See IFocusableNode.onNodeBlur. */ + onNodeBlur(): void {} + + /** See IFocusableNode.canBeFocused. */ + canBeFocused(): boolean { + return true; } /** diff --git a/core/field_checkbox.ts b/packages/blockly/core/field_checkbox.ts similarity index 97% rename from core/field_checkbox.ts rename to packages/blockly/core/field_checkbox.ts index 5ae3dfda1ae..55ed42cbf4b 100644 --- a/core/field_checkbox.ts +++ b/packages/blockly/core/field_checkbox.ts @@ -35,11 +35,6 @@ export class FieldCheckbox extends Field { */ 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. @@ -114,7 +109,7 @@ export class FieldCheckbox extends Field { super.initView(); const textElement = this.getTextElement(); - dom.addClass(textElement, 'blocklyCheckbox'); + dom.addClass(this.fieldGroup_!, 'blocklyCheckboxField'); textElement.style.display = this.value_ ? 'block' : 'none'; } diff --git a/core/field_dropdown.ts b/packages/blockly/core/field_dropdown.ts similarity index 82% rename from core/field_dropdown.ts rename to packages/blockly/core/field_dropdown.ts index bc2d2856f7d..5668a6351a8 100644 --- a/core/field_dropdown.ts +++ b/packages/blockly/core/field_dropdown.ts @@ -23,27 +23,24 @@ import { } from './field.js'; import * as fieldRegistry from './field_registry.js'; import {Menu} from './menu.js'; +import {MenuSeparator} from './menu_separator.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 {Size} from './utils/size.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. + * Magic constant used to represent a separator in a list of dropdown items. */ - static MAX_MENU_HEIGHT_VH = 0.45; + static readonly SEPARATOR = 'separator'; static ARROW_CHAR = '▾'; @@ -70,9 +67,6 @@ export class FieldDropdown extends Field { */ 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. */ @@ -136,26 +130,11 @@ export class FieldDropdown extends Field { // 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]; + this.setOptions(menuGenerator); if (config) { this.configure_(config); } - this.setValue(this.selectedOption[1]); if (validator) { this.setValidator(validator); } @@ -213,6 +192,11 @@ export class FieldDropdown extends Field { if (this.borderRect_) { dom.addClass(this.borderRect_, 'blocklyDropdownRect'); } + + if (this.fieldGroup_) { + dom.addClass(this.fieldGroup_, 'blocklyField'); + dom.addClass(this.fieldGroup_, 'blocklyDropdownField'); + } } /** @@ -277,16 +261,18 @@ export class FieldDropdown extends Field { throw new UnattachedFieldError(); } this.dropdownCreate(); + if (!this.menu_) return; + if (e && typeof e.clientX === 'number') { - this.menu_!.openingCoords = new Coordinate(e.clientX, e.clientY); + this.menu_.openingCoords = new Coordinate(e.clientX, e.clientY); } else { - this.menu_!.openingCoords = null; + 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()); + const menuElement = this.menu_.render(dropDownDiv.getContentDiv()); dom.addClass(menuElement, 'blocklyDropdownMenu'); if (this.getConstants()!.FIELD_DROPDOWN_COLOURED_DIV) { @@ -297,18 +283,15 @@ export class FieldDropdown extends Field { dropDownDiv.showPositionedByField(this, this.dropdownDispose_.bind(this)); + dropDownDiv.getContentDiv().style.height = `${this.menu_.getSize().height}px`; + // 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(); + this.menu_.focus(); if (this.selectedMenuItem) { - this.menu_!.setHighlighted(this.selectedMenuItem); - style.scrollIntoContainerView( - this.selectedMenuItem.getElement()!, - dropDownDiv.getContentDiv(), - true, - ); + this.menu_.setHighlighted(this.selectedMenuItem); } this.applyColour(); @@ -327,13 +310,19 @@ export class FieldDropdown extends Field { const options = this.getOptions(false); this.selectedMenuItem = null; for (let i = 0; i < options.length; i++) { - const [label, value] = options[i]; + const option = options[i]; + if (option === FieldDropdown.SEPARATOR) { + menu.addChild(new MenuSeparator()); + continue; + } + + const [label, value] = option; const content = (() => { - if (typeof label === 'object') { + if (isImageProperties(label)) { // Convert ImageProperties to an HTMLImageElement. - const image = new Image(label['width'], label['height']); - image.src = label['src']; - image.alt = label['alt'] || ''; + const image = new Image(label.width, label.height); + image.src = label.src; + image.alt = label.alt; return image; } return label; @@ -414,6 +403,28 @@ export class FieldDropdown extends Field { return this.generatedOptions; } + /** + * Update the options on this dropdown. This will reset the selected item to + * the first item in the list. + * + * @param menuGenerator The array of options or a generator function. + */ + setOptions(menuGenerator: MenuGenerator) { + 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]; + this.setValue(this.selectedOption[1]); + } + /** * Ensure that the input value is a valid language-neutral option. * @@ -494,7 +505,7 @@ export class FieldDropdown extends Field { // Show correct element. const option = this.selectedOption && this.selectedOption[0]; - if (option && typeof option === 'object') { + if (isImageProperties(option)) { this.renderSelectedImage(option); } else { this.renderSelectedText(); @@ -541,15 +552,19 @@ export class FieldDropdown extends Field { 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, - ); + const arrowElement = this.arrow as SVGTSpanElement; + if (arrowElement.isConnected) { + arrowWidth = dom.getTextWidth(arrowElement); + } else { + arrowWidth = dom.getFastTextWidth( + arrowElement, + 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; + this.size_ = new Size(imageWidth + arrowWidth + xPadding * 2, height); let arrowX = 0; if (block.RTL) { @@ -579,12 +594,17 @@ export class FieldDropdown extends Field { 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, - ); + let textWidth: number; + if (textElement.isConnected) { + textWidth = dom.getTextWidth(textElement); + } else { + textWidth = dom.getFastTextWidth( + textElement, + 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; @@ -595,8 +615,7 @@ export class FieldDropdown extends Field { height / 2 - this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_SIZE / 2, ); } - this.size_.width = textWidth + arrowWidth + xPadding * 2; - this.size_.height = height; + this.size_ = new Size(textWidth + arrowWidth + xPadding * 2, height); this.positionTextElement_(xPadding, textWidth); } @@ -633,7 +652,13 @@ export class FieldDropdown extends Field { /** * 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. + * an image we return the image alt text. If the selected option is + * an HTMLElement, return the title, ariaLabel, or innerText of the + * element. + * + * If you use HTMLElement options in Node.js and call this function, + * ensure that you are supplying an implementation of HTMLElement, + * such as through jsdom-global. * * @returns Selected option text. */ @@ -642,10 +667,23 @@ export class FieldDropdown extends Field { return null; } const option = this.selectedOption[0]; - if (typeof option === 'object') { - return option['alt']; + if (isImageProperties(option)) { + return option.alt; + } else if ( + typeof HTMLElement !== 'undefined' && + option instanceof HTMLElement + ) { + return option.title ?? option.ariaLabel ?? option.innerText; + } else if (typeof option === 'string') { + return option; } - return option; + + console.warn( + "Can't get text for existing dropdown option. If " + + "you're using HTMLElement dropdown options in node, ensure you're " + + 'using jsdom-global or similar.', + ); + return null; } /** @@ -680,23 +718,30 @@ export class FieldDropdown extends Field { prefix?: string; suffix?: string; } { - let hasImages = false; - const trimmedOptions = options.map(([label, value]): MenuOption => { + let hasNonTextContent = false; + const trimmedOptions = options.map((option): MenuOption => { + if (option === FieldDropdown.SEPARATOR) { + hasNonTextContent = true; + return option; + } + + const [label, value] = option; if (typeof label === 'string') { return [parsing.replaceMessageReferences(label), value]; } - hasImages = true; + hasNonTextContent = 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}; + const imageLabel = isImageProperties(label) + ? {...label, alt: parsing.replaceMessageReferences(label.alt)} + : label; return [imageLabel, value]; }); - if (hasImages || options.length < 2) return {options: trimmedOptions}; + if (hasNonTextContent || options.length < 2) { + return {options: trimmedOptions}; + } const stringOptions = trimmedOptions as [string, string][]; const stringLabels = stringOptions.map(([label]) => label); @@ -762,28 +807,31 @@ export class FieldDropdown extends Field { } let foundError = false; for (let i = 0; i < options.length; i++) { - const tuple = options[i]; - if (!Array.isArray(tuple)) { + const option = options[i]; + if (!Array.isArray(option) && option !== FieldDropdown.SEPARATOR) { foundError = true; console.error( - `Invalid option[${i}]: Each FieldDropdown option must be an array. - Found: ${tuple}`, + `Invalid option[${i}]: Each FieldDropdown option must be an array or + the string literal 'separator'. Found: ${option}`, ); - } else if (typeof tuple[1] !== 'string') { + } else if (typeof option[1] !== 'string') { foundError = true; console.error( - `Invalid option[${i}]: Each FieldDropdown option id must be a string. - Found ${tuple[1]} in: ${tuple}`, + `Invalid option[${i}]: Each FieldDropdown option id must be a string. + Found ${option[1]} in: ${option}`, ); } else if ( - tuple[0] && - typeof tuple[0] !== 'string' && - typeof tuple[0].src !== 'string' + option[0] && + typeof option[0] !== 'string' && + !isImageProperties(option[0]) && + !( + typeof HTMLElement !== 'undefined' && option[0] instanceof HTMLElement + ) ) { foundError = true; console.error( - `Invalid option[${i}]: Each FieldDropdown option must have a string - label or image description. Found ${tuple[0]} in: ${tuple}`, + `Invalid option[${i}]: Each FieldDropdown option must have a string + label, image description, or HTML element. Found ${option[0]} in: ${option}`, ); } } @@ -793,6 +841,27 @@ export class FieldDropdown extends Field { } } +/** + * Returns whether or not an object conforms to the ImageProperties interface. + * + * @param obj The object to test. + * @returns True if the object conforms to ImageProperties, otherwise false. + */ +function isImageProperties(obj: any): obj is ImageProperties { + return ( + obj && + typeof obj === 'object' && + 'src' in obj && + typeof obj.src === 'string' && + 'alt' in obj && + typeof obj.alt === 'string' && + 'width' in obj && + typeof obj.width === 'number' && + 'height' in obj && + typeof obj.height === 'number' + ); +} + /** * Definition of a human-readable image dropdown option. */ @@ -804,11 +873,15 @@ export interface ImageProperties { } /** - * 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. + * An individual option in the dropdown menu. Can be either the string literal + * `separator` for a menu separator item, or an array for normal action menu + * items. In the latter case, the first element is the human-readable value + * (text, ImageProperties object, or HTML element), and the second element is + * the language-neutral value. */ -export type MenuOption = [string | ImageProperties, string]; +export type MenuOption = + | [string | ImageProperties | HTMLElement, string] + | 'separator'; /** * A function that generates an array of menu options for FieldDropdown diff --git a/core/field_image.ts b/packages/blockly/core/field_image.ts similarity index 94% rename from core/field_image.ts rename to packages/blockly/core/field_image.ts index 6e83e3405c6..01133c20340 100644 --- a/core/field_image.ts +++ b/packages/blockly/core/field_image.ts @@ -27,7 +27,6 @@ export class FieldImage extends Field { * 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. */ @@ -151,6 +150,10 @@ export class FieldImage extends Field { this.value_ as string, ); + if (this.fieldGroup_) { + dom.addClass(this.fieldGroup_, 'blocklyImageField'); + } + if (this.clickHandler) { this.imageElement.style.cursor = 'pointer'; } @@ -209,6 +212,17 @@ export class FieldImage extends Field { } } + /** + * Check whether this field should be clickable. + * + * @returns Whether this field is clickable. + */ + isClickable(): boolean { + // Images are only clickable if they have a click handler and fulfill the + // contract to be clickable: enabled and attached to an editable block. + return super.isClickable() && !!this.clickHandler; + } + /** * If field click is called, and click handler defined, * call the handler. diff --git a/core/field_input.ts b/packages/blockly/core/field_input.ts similarity index 86% rename from core/field_input.ts rename to packages/blockly/core/field_input.ts index 2c8a48e6760..55383a4c1d2 100644 --- a/core/field_input.ts +++ b/packages/blockly/core/field_input.ts @@ -27,6 +27,8 @@ import { FieldValidator, UnattachedFieldError, } from './field.js'; +import {getFocusManager} from './focus_manager.js'; +import type {IFocusableNode} from './interfaces/i_focusable_node.js'; import {Msg} from './msg.js'; import * as renderManagement from './render_management.js'; import * as aria from './utils/aria.js'; @@ -43,6 +45,11 @@ import type {WorkspaceSvg} from './workspace_svg.js'; */ type InputTypes = string | number; +/** + * The minimum width of an input field. + */ +const MINIMUM_WIDTH = 14; + /** * Abstract class for an editable input field. * @@ -82,8 +89,8 @@ export abstract class FieldInput extends Field< /** Key down event data. */ private onKeyDownWrapper: browserEvents.Data | null = null; - /** Key input event data. */ - private onKeyInputWrapper: browserEvents.Data | null = null; + /** Input element input event data. */ + private onInputWrapper: browserEvents.Data | null = null; /** * Whether the field should consider the whole parent block to be its click @@ -100,8 +107,23 @@ export abstract class FieldInput extends Field< */ override SERIALIZABLE = true; - /** Mouse cursor style when over the hotspot that initiates the editor. */ - override CURSOR = 'text'; + protected override set size_(newValue: Size) { + // Although this appears to be a no-op, it must exist since the getter is + // overridden below. + super.size_ = newValue; + } + + /** + * Returns the size of this field, with a minimum width of 14. + */ + protected override get size_() { + const s = super.size_; + if (s.width < MINIMUM_WIDTH) { + s.width = MINIMUM_WIDTH; + } + + return s; + } /** * @param value The initial value of the field. Should cast to a string. @@ -149,9 +171,13 @@ export abstract class FieldInput extends Field< if (this.isFullBlockField()) { this.clickTarget_ = (this.sourceBlock_ as BlockSvg).getSvgRoot(); } + + if (this.fieldGroup_) { + dom.addClass(this.fieldGroup_, 'blocklyInputField'); + } } - protected override isFullBlockField(): boolean { + override isFullBlockField(): boolean { const block = this.getSourceBlock(); if (!block) throw new UnattachedFieldError(); @@ -330,8 +356,16 @@ export abstract class FieldInput extends Field< * undefined if triggered programmatically. * @param quietInput True if editor should be created without focus. * Defaults to false. + * @param manageEphemeralFocus Whether ephemeral focus should be managed as + * part of the editor's inline editor (when the inline editor is shown). + * Callers must manage ephemeral focus themselves if this is false. + * Defaults to true. */ - protected override showEditor_(_e?: Event, quietInput = false) { + protected override showEditor_( + _e?: Event, + quietInput = false, + manageEphemeralFocus: boolean = true, + ) { this.workspace_ = (this.sourceBlock_ as BlockSvg).workspace; if ( !quietInput && @@ -340,7 +374,7 @@ export abstract class FieldInput extends Field< ) { this.showPromptEditor(); } else { - this.showInlineEditor(quietInput); + this.showInlineEditor(quietInput, manageEphemeralFocus); } } @@ -367,8 +401,10 @@ export abstract class FieldInput extends Field< * Create and show a text input editor that sits directly over the text input. * * @param quietInput True if editor should be created without focus. + * @param manageEphemeralFocus Whether ephemeral focus should be managed as + * part of the field's inline editor (widget div). */ - private showInlineEditor(quietInput: boolean) { + private showInlineEditor(quietInput: boolean, manageEphemeralFocus: boolean) { const block = this.getSourceBlock(); if (!block) { throw new UnattachedFieldError(); @@ -378,6 +414,7 @@ export abstract class FieldInput extends Field< block.RTL, this.widgetDispose_.bind(this), this.workspace_, + manageEphemeralFocus, ); this.htmlInput_ = this.widgetCreate_() as HTMLInputElement; this.isBeingEdited_ = true; @@ -406,7 +443,7 @@ export abstract class FieldInput extends Field< const clickTarget = this.getClickTarget_(); if (!clickTarget) throw new Error('A click target has not been set.'); - dom.addClass(clickTarget, 'editing'); + dom.addClass(clickTarget, 'blocklyEditing'); const htmlInput = document.createElement('input'); htmlInput.className = 'blocklyHtmlInput'; @@ -416,7 +453,7 @@ export abstract class FieldInput extends Field< 'spellcheck', this.spellcheck_ as AnyDuringMigration, ); - const scale = this.workspace_!.getScale(); + const scale = this.workspace_!.getAbsoluteScale(); const fontSize = this.getConstants()!.FIELD_TEXT_FONTSIZE * scale + 'pt'; div!.style.fontSize = fontSize; htmlInput.style.fontSize = fontSize; @@ -501,7 +538,7 @@ export abstract class FieldInput extends Field< const clickTarget = this.getClickTarget_(); if (!clickTarget) throw new Error('A click target has not been set.'); - dom.removeClass(clickTarget, 'editing'); + dom.removeClass(clickTarget, 'blocklyEditing'); } /** @@ -525,7 +562,7 @@ export abstract class FieldInput extends Field< this.onHtmlInputKeyDown_, ); // Resize after every input change. - this.onKeyInputWrapper = browserEvents.conditionalBind( + this.onInputWrapper = browserEvents.conditionalBind( htmlInput, 'input', this, @@ -539,9 +576,9 @@ export abstract class FieldInput extends Field< browserEvents.unbind(this.onKeyDownWrapper); this.onKeyDownWrapper = null; } - if (this.onKeyInputWrapper) { - browserEvents.unbind(this.onKeyInputWrapper); - this.onKeyInputWrapper = null; + if (this.onInputWrapper) { + browserEvents.unbind(this.onInputWrapper); + this.onInputWrapper = null; } } @@ -562,17 +599,42 @@ export abstract class FieldInput extends Field< 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(); + const cursor = this.workspace_?.getCursor(); + + const isValidDestination = (node: IFocusableNode | null) => + (node instanceof FieldInput || + (node instanceof BlockSvg && node.isSimpleReporter())) && + node !== this.getSourceBlock(); + + let target = e.shiftKey + ? cursor?.getPreviousNode(this, isValidDestination, false) + : cursor?.getNextNode(this, isValidDestination, false); + target = + target instanceof BlockSvg && target.isSimpleReporter() + ? target.getFields().next().value + : target; + + if (target instanceof FieldInput) { + WidgetDiv.hideIfOwner(this); + dropDownDiv.hideWithoutAnimation(); + const targetSourceBlock = target.getSourceBlock(); + if ( + target.isFullBlockField() && + targetSourceBlock && + targetSourceBlock instanceof BlockSvg + ) { + getFocusManager().focusNode(targetSourceBlock); + } else getFocusManager().focusNode(target); + target.showEditor(); + } } } /** * Handle a change to the editor. * - * @param _e Keyboard event. + * @param _e InputEvent. */ private onHtmlInputChange(_e: Event) { // Intermediate value changes from user input are not confirmed until the @@ -674,12 +736,20 @@ export abstract class FieldInput extends Field< } /** - * Returns whether or not the field is tab navigable. + * Position a field's text element after a size change. This handles both LTR + * and RTL positioning. * - * @returns True if the field is tab navigable. + * @param xMargin x offset to use when positioning the text element. + * @param contentWidth The content width. */ - override isTabNavigable(): boolean { - return true; + protected override positionTextElement_( + xMargin: number, + contentWidth: number, + ) { + const effectiveWidth = xMargin * 2 + contentWidth; + const delta = + effectiveWidth < MINIMUM_WIDTH ? (MINIMUM_WIDTH - effectiveWidth) / 2 : 0; + super.positionTextElement_(xMargin + delta, contentWidth); } /** diff --git a/core/field_label.ts b/packages/blockly/core/field_label.ts similarity index 97% rename from core/field_label.ts rename to packages/blockly/core/field_label.ts index 2b0ae1eba49..236154cc7b1 100644 --- a/core/field_label.ts +++ b/packages/blockly/core/field_label.ts @@ -74,6 +74,9 @@ export class FieldLabel extends Field { if (this.class) { dom.addClass(this.getTextElement(), this.class); } + if (this.fieldGroup_) { + dom.addClass(this.fieldGroup_, 'blocklyLabelField'); + } } /** diff --git a/core/field_label_serializable.ts b/packages/blockly/core/field_label_serializable.ts similarity index 100% rename from core/field_label_serializable.ts rename to packages/blockly/core/field_label_serializable.ts diff --git a/core/field_number.ts b/packages/blockly/core/field_number.ts similarity index 97% rename from core/field_number.ts rename to packages/blockly/core/field_number.ts index 0641b9ae32b..7e36591753e 100644 --- a/core/field_number.ts +++ b/packages/blockly/core/field_number.ts @@ -19,6 +19,7 @@ import { } from './field_input.js'; import * as fieldRegistry from './field_registry.js'; import * as aria from './utils/aria.js'; +import * as dom from './utils/dom.js'; /** * Class for an editable number field. @@ -307,6 +308,19 @@ export class FieldNumber extends FieldInput { return htmlInput; } + /** + * Initialize the field's DOM. + * + * @override + */ + + public override initView() { + super.initView(); + if (this.fieldGroup_) { + dom.addClass(this.fieldGroup_, 'blocklyNumberField'); + } + } + /** * Construct a FieldNumber from a JSON arg object. * diff --git a/core/field_registry.ts b/packages/blockly/core/field_registry.ts similarity index 94% rename from core/field_registry.ts rename to packages/blockly/core/field_registry.ts index 06bb9acd045..e02ece75c96 100644 --- a/core/field_registry.ts +++ b/packages/blockly/core/field_registry.ts @@ -56,11 +56,11 @@ export interface RegistrableField { * @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. + * @throws {Error} if the type name is empty 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); + registry.register(registry.Type.FIELD, type, fieldClass, true); } /** diff --git a/core/field_textinput.ts b/packages/blockly/core/field_textinput.ts similarity index 95% rename from core/field_textinput.ts rename to packages/blockly/core/field_textinput.ts index 39bdca97056..2b896ad47be 100644 --- a/core/field_textinput.ts +++ b/packages/blockly/core/field_textinput.ts @@ -21,6 +21,7 @@ import { FieldInputValidator, } from './field_input.js'; import * as fieldRegistry from './field_registry.js'; +import * as dom from './utils/dom.js'; import * as parsing from './utils/parsing.js'; /** @@ -49,6 +50,13 @@ export class FieldTextInput extends FieldInput { super(value, validator, config); } + override initView() { + super.initView(); + if (this.fieldGroup_) { + dom.addClass(this.fieldGroup_, 'blocklyTextInputField'); + } + } + /** * Ensure that the input value casts to a valid string. * diff --git a/core/field_variable.ts b/packages/blockly/core/field_variable.ts similarity index 83% rename from core/field_variable.ts rename to packages/blockly/core/field_variable.ts index 539557256b6..aa4fdfe310f 100644 --- a/core/field_variable.ts +++ b/packages/blockly/core/field_variable.ts @@ -23,13 +23,14 @@ import { MenuOption, } from './field_dropdown.js'; import * as fieldRegistry from './field_registry.js'; +import {IVariableModel, IVariableState} from './interfaces/i_variable_model.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 dom from './utils/dom.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'; @@ -48,10 +49,9 @@ export class FieldVariable extends FieldDropdown { * dropdown. */ variableTypes: string[] | null = []; - protected override size_: Size; /** The variable model associated with this field. */ - private variable: VariableModel | null = null; + private variable: IVariableModel | null = null; /** * Serializable fields are saved by the serializer, non-serializable fields @@ -69,7 +69,8 @@ export class FieldVariable extends FieldDropdown { * 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. + * dropdown. Pass `null` to include all types that exist on the + * workspace. 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. @@ -81,7 +82,7 @@ export class FieldVariable extends FieldDropdown { constructor( varName: string | null | typeof Field.SKIP_SETUP, validator?: FieldVariableValidator, - variableTypes?: string[], + variableTypes?: string[] | null, defaultType?: string, config?: FieldVariableConfig, ) { @@ -148,6 +149,11 @@ export class FieldVariable extends FieldDropdown { this.doValueUpdate_(variable.getId()); } + override initView() { + super.initView(); + dom.addClass(this.fieldGroup_!, 'blocklyVariableField'); + } + override shouldAddBorderRect_() { const block = this.getSourceBlock(); if (!block) { @@ -190,12 +196,12 @@ export class FieldVariable extends FieldDropdown { ); // This should never happen :) - if (variableType !== null && variableType !== variable.type) { + if (variableType !== null && variableType !== variable.getType()) { throw Error( "Serialized variable type with id '" + variable.getId() + "' had type " + - variable.type + + variable.getType() + ', and ' + 'does not match variable field that references it: ' + Xml.domToText(fieldElement) + @@ -218,9 +224,9 @@ export class FieldVariable extends FieldDropdown { this.initModel(); fieldElement.id = this.variable!.getId(); - fieldElement.textContent = this.variable!.name; - if (this.variable!.type) { - fieldElement.setAttribute('variabletype', this.variable!.type); + fieldElement.textContent = this.variable!.getName(); + if (this.variable!.getType()) { + fieldElement.setAttribute('variabletype', this.variable!.getType()); } return fieldElement; } @@ -243,8 +249,8 @@ export class FieldVariable extends FieldDropdown { this.initModel(); const state = {'id': this.variable!.getId()}; if (doFullSerialization) { - (state as AnyDuringMigration)['name'] = this.variable!.name; - (state as AnyDuringMigration)['type'] = this.variable!.type; + (state as AnyDuringMigration)['name'] = this.variable!.getName(); + (state as AnyDuringMigration)['type'] = this.variable!.getType(); } return state; } @@ -301,7 +307,7 @@ export class FieldVariable extends FieldDropdown { * is selected. */ override getText(): string { - return this.variable ? this.variable.name : ''; + return this.variable ? this.variable.getName() : ''; } /** @@ -312,10 +318,19 @@ export class FieldVariable extends FieldDropdown { * @returns The selected variable, or null if none was selected. * @internal */ - getVariable(): VariableModel | null { + getVariable(): IVariableModel | null { return this.variable; } + /** + * Gets the type of this field's default variable. + * + * @returns The default type for this variable field. + */ + protected getDefaultType(): string { + return this.defaultType; + } + /** * Gets the validation function for this field, or null if not set. * Returns null if the variable is not set, because validators should not @@ -359,7 +374,7 @@ export class FieldVariable extends FieldDropdown { return null; } // Type Checks. - const type = variable.type; + const type = variable.getType(); if (!this.typeIsAllowed(type)) { console.warn("Variable type doesn't match this field! Type was " + type); return null; @@ -407,25 +422,27 @@ export class FieldVariable extends FieldDropdown { * 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(); - } + if (this.variableTypes) return this.variableTypes; + + if (!this.sourceBlock_ || this.sourceBlock_.isDeadOrDying()) { + // We should include all types in the block's workspace, + // but the block is dead so just give up. + return ['']; } - 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', - ); + + // If variableTypes is null, return all variable types in the workspace. + let allTypes = this.sourceBlock_.workspace.getVariableMap().getTypes(); + if (this.sourceBlock_.isInFlyout) { + // If this block is in a flyout, we also need to check the potential variables + const potentialMap = + this.sourceBlock_.workspace.getPotentialVariableMap(); + if (!potentialMap) return allTypes; + allTypes = Array.from(new Set([...allTypes, ...potentialMap.getTypes()])); } - return variableTypes; + + return allTypes; } /** @@ -439,11 +456,15 @@ export class FieldVariable extends FieldDropdown { * 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. + const name = this.getText(); if (Array.isArray(variableTypes)) { + if (variableTypes.length === 0) { + // Throw an error if variableTypes is an empty list. + throw Error( + `'variableTypes' of field variable ${name} was an empty list. If you want to include all variable types, pass 'null' instead.`, + ); + } + // Make sure the default type is valid. let isInArray = false; for (let i = 0; i < variableTypes.length; i++) { @@ -461,8 +482,7 @@ export class FieldVariable extends FieldDropdown { } } else if (variableTypes !== null) { throw Error( - "'variableTypes' was not an array in the definition of " + - 'a FieldVariable', + `'variableTypes' was not an array or null in the definition of FieldVariable ${name}`, ); } // Only update the field once all checks pass. @@ -493,16 +513,14 @@ export class FieldVariable extends FieldDropdown { const id = menuItem.getValue(); // Handle special cases. if (this.sourceBlock_ && !this.sourceBlock_.isDeadOrDying()) { - if (id === internalConstants.RENAME_VARIABLE_ID) { + if (id === internalConstants.RENAME_VARIABLE_ID && this.variable) { // Rename variable. - Variables.renameVariable( - this.sourceBlock_.workspace, - this.variable as VariableModel, - ); + Variables.renameVariable(this.sourceBlock_.workspace, this.variable); return; - } else if (id === internalConstants.DELETE_VARIABLE_ID) { + } else if (id === internalConstants.DELETE_VARIABLE_ID && this.variable) { // Delete variable. - this.sourceBlock_.workspace.deleteVariableById(this.variable!.getId()); + const workspace = this.variable.getWorkspace(); + Variables.deleteVariable(workspace, this.variable, this.sourceBlock_); return; } } @@ -554,24 +572,37 @@ export class FieldVariable extends FieldDropdown { ); } const name = this.getText(); - let variableModelList: VariableModel[] = []; - if (this.sourceBlock_ && !this.sourceBlock_.isDeadOrDying()) { + let variableModelList: IVariableModel[] = []; + const sourceBlock = this.getSourceBlock(); + if (sourceBlock && !sourceBlock.isDeadOrDying()) { + const workspace = sourceBlock.workspace; 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); + const variables = workspace + .getVariableMap() + .getVariablesOfType(variableType); variableModelList = variableModelList.concat(variables); + if (workspace.isFlyout) { + variableModelList = variableModelList.concat( + workspace + .getPotentialVariableMap() + ?.getVariablesOfType(variableType) ?? [], + ); + } } } - variableModelList.sort(VariableModel.compareByName); + variableModelList.sort(Variables.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[i] = [ + variableModelList[i].getName(), + variableModelList[i].getId(), + ]; } options.push([ Msg['RENAME_VARIABLE'], diff --git a/core/flyout_base.ts b/packages/blockly/core/flyout_base.ts similarity index 54% rename from core/flyout_base.ts rename to packages/blockly/core/flyout_base.ts index 96d2b27fdcb..fb774bd61cd 100644 --- a/core/flyout_base.ts +++ b/packages/blockly/core/flyout_base.ts @@ -11,53 +11,41 @@ */ // 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 {FlyoutItem} from './flyout_item.js'; import {FlyoutMetricsManager} from './flyout_metrics_manager.js'; +import {FlyoutNavigator} from './flyout_navigator.js'; +import {FlyoutSeparator, SeparatorAxis} from './flyout_separator.js'; import {IAutoHideable} from './interfaces/i_autohideable.js'; import type {IFlyout} from './interfaces/i_flyout.js'; +import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js'; +import {IFocusableNode} from './interfaces/i_focusable_node.js'; +import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; import type {Options} from './options.js'; +import * as registry from './registry.js'; import * as renderManagement from './render_management.js'; import {ScrollbarPair} from './scrollbar_pair.js'; +import {SEPARATOR_TYPE} from './separator_flyout_inflater.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 + implements IAutoHideable, IFlyout, IFocusableNode { /** * Position the flyout. @@ -85,12 +73,11 @@ export abstract class Flyout protected abstract setMetrics_(xyRatio: {x?: number; y?: number}): void; /** - * Lay out the blocks in the flyout. + * Lay out the elements in the flyout. * - * @param contents The blocks and buttons to lay out. - * @param gaps The visible gaps between blocks. + * @param contents The flyout elements to lay out. */ - protected abstract layout_(contents: FlyoutItem[], gaps: number[]): void; + protected abstract layout_(contents: FlyoutItem[]): void; /** * Scroll the flyout. @@ -100,8 +87,8 @@ export abstract class Flyout protected abstract wheel_(e: WheelEvent): void; /** - * Compute height of flyout. Position mat under each block. - * For RTL: Lay out the blocks right-aligned. + * Compute bounds of flyout. + * For RTL: Lay out the elements right-aligned. */ protected abstract reflowInternal_(): void; @@ -124,11 +111,6 @@ export abstract class Flyout */ abstract scrollToStart(): void; - /** - * The type of a flyout content item. - */ - static FlyoutItemType = FlyoutItemType; - protected workspace_: WorkspaceSvg; RTL: boolean; /** @@ -148,43 +130,21 @@ export abstract class Flyout /** * Function that will be registered as a change listener on the workspace - * to reflow when blocks in the flyout workspace change. + * to reflow when elements 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. + * If true, prevents the reflow wrapper from running. Used to prevent infinite + * recursion. */ - private filterWrapper: ((e: AbstractEvent) => void) | null = null; + private inhibitReflowWrapper = false; /** - * 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. + * List of flyout elements. */ 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; /** @@ -194,11 +154,6 @@ export abstract class Flyout */ targetWorkspace!: WorkspaceSvg; - /** - * A list of blocks that can be reused. - */ - private recycledBlocks: BlockSvg[] = []; - /** * Does the flyout automatically close when a block is created? */ @@ -213,7 +168,6 @@ export abstract class Flyout * Whether the workspace containing this flyout is visible. */ private containerVisible = true; - protected rectMap_: WeakMap; /** * Corner radius of the flyout background. @@ -271,6 +225,13 @@ export abstract class Flyout * The root SVG group for the button or label. */ protected svgGroup_: SVGGElement | null = null; + + /** + * Map from flyout content type to the corresponding inflater class + * responsible for creating concrete instances of the content type. + */ + protected inflaters = new Map(); + /** * @param workspaceOptions Dictionary of options for the * workspace. @@ -287,6 +248,7 @@ export abstract class Flyout this.workspace_.internalIsFlyout = true; // Keep the workspace visibility consistent with the flyout's visibility. this.workspace_.setVisible(this.visible); + this.workspace_.setNavigator(new FlyoutNavigator(this)); /** * The unique id for this component that is used to register with the @@ -310,15 +272,7 @@ export abstract class Flyout 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. + * Margin around the edges of the elements in the flyout. */ this.MARGIN = this.CORNER_RADIUS; @@ -372,6 +326,7 @@ export abstract class Flyout this.workspace_ .getThemeManager() .subscribe(this.svgBackground_, 'flyoutOpacity', 'fill-opacity'); + return this.svgGroup_; } @@ -401,10 +356,10 @@ export abstract class Flyout 'wheel', this, this.wheel_, + false, + {passive: false}, ), ); - this.filterWrapper = this.filterForCapacity.bind(this); - this.targetWorkspace.addChangeListener(this.filterWrapper); // Dragging the flyout up and down. this.boundEvents.push( @@ -448,9 +403,6 @@ export abstract class Flyout 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(); @@ -570,16 +522,16 @@ export abstract class Flyout } /** - * Get the list of buttons and blocks of the current flyout. + * Get the list of elements of the current flyout. * - * @returns The array of flyout buttons and blocks. + * @returns The array of flyout elements. */ getContents(): FlyoutItem[] { return this.contents; } /** - * Store the list of buttons and blocks on the flyout. + * Store the list of elements on the flyout. * * @param contents - The array of items for the flyout. */ @@ -654,16 +606,11 @@ export abstract class Flyout 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. + // Do NOT delete the flyout contents here. Wait until Flyout.show. // https://neil.fraser.name/news/2014/08/09/ } @@ -676,6 +623,7 @@ export abstract class Flyout */ show(flyoutDef: toolbox.FlyoutDefinition | string) { this.workspace_.setResizesEnabled(false); + eventUtils.setRecordUndo(false); this.hide(); this.clearOldBlocks(); @@ -691,26 +639,32 @@ export abstract class Flyout renderManagement.triggerQueuedRenders(this.workspace_); - this.setContents(flyoutInfo.contents); + this.setContents(flyoutInfo); - this.layout_(flyoutInfo.contents, flyoutInfo.gaps); + this.layout_(flyoutInfo); if (this.horizontalLayout) { this.height_ = 0; } else { this.width_ = 0; } - this.workspace_.setResizesEnabled(true); this.reflow(); + eventUtils.setRecordUndo(true); + this.workspace_.setResizesEnabled(true); - this.filterForCapacity(); - - // Correctly position the flyout's scrollbar when it opens. - this.position(); - - this.reflowWrapper = this.reflow.bind(this); + // Listen for block change events, and reflow the flyout in response. This + // accommodates e.g. resizing a non-autoclosing flyout in response to the + // user typing long strings into fields on the blocks in the flyout. + this.reflowWrapper = (event) => { + if (this.inhibitReflowWrapper) return; + if ( + event.type === EventType.BLOCK_CHANGE || + event.type === EventType.BLOCK_FIELD_INTERMEDIATE_CHANGE + ) { + this.reflow(); + } + }; this.workspace_.addChangeListener(this.reflowWrapper); - this.emptyRecycledBlocks(); } /** @@ -719,15 +673,12 @@ export abstract class 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. + * @returns The list of contents needed to lay out the flyout. */ - private createFlyoutInfo(parsedContent: toolbox.FlyoutItemInfoArray): { - contents: FlyoutItem[]; - gaps: number[]; - } { + private createFlyoutInfo( + parsedContent: toolbox.FlyoutItemInfoArray, + ): FlyoutItem[] { 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) { @@ -736,44 +687,59 @@ export abstract class Flyout const flyoutDef = this.getDynamicCategoryContents(categoryName); const parsedDynamicContent = toolbox.convertFlyoutDefToJsonArray(flyoutDef); - const {contents: dynamicContents, gaps: dynamicGaps} = - this.createFlyoutInfo(parsedDynamicContent); - contents.push(...dynamicContents); - gaps.push(...dynamicGaps); + contents.push(...this.createFlyoutInfo(parsedDynamicContent)); } - 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; + const type = info['kind'].toLowerCase(); + const inflater = this.getInflaterForType(type); + if (inflater) { + contents.push(inflater.load(info, this)); + const gap = inflater.gapForItem(info, defaultGap); + if (gap) { + contents.push( + new FlyoutItem( + new FlyoutSeparator( + gap, + this.horizontalLayout ? SeparatorAxis.X : SeparatorAxis.Y, + ), + SEPARATOR_TYPE, + ), + ); } } } - return {contents: contents, gaps: gaps}; + return this.normalizeSeparators(contents); + } + + /** + * Updates and returns the provided list of flyout contents to flatten + * separators as needed. + * + * When multiple separators occur one after another, the value of the last one + * takes precedence and the earlier separators in the group are removed. + * + * @param contents The list of flyout contents to flatten separators in. + * @returns An updated list of flyout contents with only one separator between + * each non-separator item. + */ + protected normalizeSeparators(contents: FlyoutItem[]): FlyoutItem[] { + for (let i = contents.length - 1; i > 0; i--) { + const elementType = contents[i].getType().toLowerCase(); + const previousElementType = contents[i - 1].getType().toLowerCase(); + if ( + elementType === SEPARATOR_TYPE && + previousElementType === SEPARATOR_TYPE + ) { + // Remove previousElement from the array, shifting the current element + // forward as a result. This preserves the behavior where explicit + // separator elements override the value of prior implicit (or explicit) + // separator elements. + contents.splice(i - 1, 1); + } + } + + return contents; } /** @@ -800,287 +766,18 @@ export abstract class Flyout } /** - * 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. + * Delete elements 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; + this.getContents().forEach((item) => { + const inflater = this.getInflaterForType(item.getType()); + inflater?.disposeItem(item); + }); // 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. * @@ -1103,7 +800,7 @@ export abstract class Flyout * @internal */ isBlockCreatable(block: BlockSvg): boolean { - return block.isEnabled(); + return block.isEnabled() && !this.getTargetWorkspace().isReadOnly(); } /** @@ -1115,166 +812,32 @@ export abstract class Flyout * @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(); + const targetWorkspace = this.targetWorkspace; + const svgRootOld = originalBlock.getSvgRoot(); + if (!svgRootOld) { + throw Error('oldBlock is not rendered'); } - 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), - ); - } + // Clone the block. + const json = this.serializeBlock(originalBlock); + // Normally this resizes leading to weird jumps. Save it for terminateDrag. + targetWorkspace.setResizesEnabled(false); + const block = blocks.appendInternal(json, targetWorkspace, { + recordUndo: true, + }) as BlockSvg; - /** - * 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(); - } - } - } + this.positionNewBlock(originalBlock, block); + targetWorkspace.hideChaff(); + return block; } /** - * Reflow blocks and their mats. + * Reflow flyout contents. */ reflow() { - if (this.reflowWrapper) { - this.workspace_.removeChangeListener(this.reflowWrapper); - } + this.inhibitReflowWrapper = true; this.reflowInternal_(); - if (this.reflowWrapper) { - this.workspace_.addChangeListener(this.reflowWrapper); - } + this.inhibitReflowWrapper = false; } /** @@ -1288,30 +851,6 @@ export abstract class Flyout : 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. * @@ -1364,13 +903,111 @@ export abstract class Flyout // 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; + /** + * Returns the inflater responsible for constructing items of the given type. + * + * @param type The type of flyout content item to provide an inflater for. + * @returns An inflater object for the given type, or null if no inflater + * is registered for that type. + */ + protected getInflaterForType(type: string): IFlyoutInflater | null { + if (this.inflaters.has(type)) { + return this.inflaters.get(type) ?? null; + } + + const InflaterClass = registry.getClass( + registry.Type.FLYOUT_INFLATER, + type, + ); + if (InflaterClass) { + const inflater = new InflaterClass(); + this.inflaters.set(type, inflater); + return inflater; + } + + return null; + } + + /** + * See IFocusableNode.getFocusableElement. + * + * @deprecated v12: Use the Flyout's workspace for focus operations, instead. + */ + getFocusableElement(): HTMLElement | SVGElement { + throw new Error('Flyouts are not directly focusable.'); + } + + /** + * See IFocusableNode.getFocusableTree. + * + * @deprecated v12: Use the Flyout's workspace for focus operations, instead. + */ + getFocusableTree(): IFocusableTree { + throw new Error('Flyouts are not directly focusable.'); + } + + /** See IFocusableNode.onNodeFocus. */ + onNodeFocus(): void {} + + /** See IFocusableNode.onNodeBlur. */ + onNodeBlur(): void {} + + /** See IFocusableNode.canBeFocused. */ + canBeFocused(): boolean { + return false; + } + + /** + * See IFocusableNode.getRootFocusableNode. + * + * @deprecated v12: Use the Flyout's workspace for focus operations, instead. + */ + getRootFocusableNode(): IFocusableNode { + throw new Error('Flyouts are not directly focusable.'); + } + + /** + * See IFocusableNode.getRestoredFocusableNode. + * + * @deprecated v12: Use the Flyout's workspace for focus operations, instead. + */ + getRestoredFocusableNode( + _previousNode: IFocusableNode | null, + ): IFocusableNode | null { + throw new Error('Flyouts are not directly focusable.'); + } + + /** + * See IFocusableNode.getNestedTrees. + * + * @deprecated v12: Use the Flyout's workspace for focus operations, instead. + */ + getNestedTrees(): Array { + throw new Error('Flyouts are not directly focusable.'); + } + + /** + * See IFocusableNode.lookUpFocusableNode. + * + * @deprecated v12: Use the Flyout's workspace for focus operations, instead. + */ + lookUpFocusableNode(_id: string): IFocusableNode | null { + throw new Error('Flyouts are not directly focusable.'); + } + + /** See IFocusableTree.onTreeFocus. */ + onTreeFocus( + _node: IFocusableNode, + _previousTree: IFocusableTree | null, + ): void {} + + /** + * See IFocusableNode.onTreeBlur. + * + * @deprecated v12: Use the Flyout's workspace for focus operations, instead. + */ + onTreeBlur(_nextTree: IFocusableTree | null): void { + throw new Error('Flyouts are not directly focusable.'); + } } diff --git a/core/flyout_button.ts b/packages/blockly/core/flyout_button.ts similarity index 71% rename from core/flyout_button.ts rename to packages/blockly/core/flyout_button.ts index b03a8d9615c..ef8d0b33277 100644 --- a/core/flyout_button.ts +++ b/packages/blockly/core/flyout_button.ts @@ -11,12 +11,17 @@ */ // 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 type {IBoundedElement} from './interfaces/i_bounded_element.js'; +import type {IFocusableNode} from './interfaces/i_focusable_node.js'; +import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; +import type {IRenderedElement} from './interfaces/i_rendered_element.js'; +import {idGenerator} from './utils.js'; import {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 * as style from './utils/style.js'; import {Svg} from './utils/svg.js'; import type * as toolbox from './utils/toolbox.js'; @@ -25,7 +30,9 @@ import type {WorkspaceSvg} from './workspace_svg.js'; /** * Class for a button or label in the flyout. */ -export class FlyoutButton implements IASTNodeLocationSvg { +export class FlyoutButton + implements IBoundedElement, IRenderedElement, IFocusableNode +{ /** The horizontal margin around the text in the button. */ static TEXT_MARGIN_X = 5; @@ -35,13 +42,16 @@ export class FlyoutButton implements IASTNodeLocationSvg { /** The radius of the flyout button's borders. */ static BORDER_RADIUS = 4; + /** The key to the function called when this button is activated. */ + readonly callbackKey: string; + 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; + private onMouseDownWrapper: browserEvents.Data; + private onMouseUpWrapper: browserEvents.Data; info: toolbox.ButtonOrLabelInfo; /** The width of the button's rect. */ @@ -51,7 +61,7 @@ export class FlyoutButton implements IASTNodeLocationSvg { height = 0; /** The root SVG group for the button or label. */ - private svgGroup: SVGGElement | null = null; + private svgGroup: SVGGElement; /** The SVG element with the text of the label or button. */ private svgText: SVGTextElement | null = null; @@ -62,6 +72,9 @@ export class FlyoutButton implements IASTNodeLocationSvg { */ cursorSvg: SVGElement | null = null; + /** The unique ID for this FlyoutButton. */ + private id: string; + /** * @param workspace The workspace in which to place this button. * @param targetWorkspace The flyout's target workspace. @@ -79,12 +92,13 @@ export class FlyoutButton implements IASTNodeLocationSvg { this.position = new Coordinate(0, 0); - /** The key to the function called when this button is clicked. */ + /** + * The key to the function called when this button is activated. + * Check both the uppercase and lowercase version, because the docs + * say `callbackKey` but the type says `callbackkey`. + */ this.callbackKey = - (json as AnyDuringMigration)[ - 'callbackKey' - ] /* Check the lower case version - too to satisfy IE */ || + (json as AnyDuringMigration)['callbackKey'] || (json as AnyDuringMigration)['callbackkey']; /** If specified, a CSS class to add to this button. */ @@ -92,14 +106,6 @@ export class FlyoutButton implements IASTNodeLocationSvg { /** 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'; @@ -107,9 +113,10 @@ export class FlyoutButton implements IASTNodeLocationSvg { cssClass += ' ' + this.cssClass; } + this.id = idGenerator.getNextUniqueId(); this.svgGroup = dom.createSvgElement( Svg.G, - {'class': cssClass}, + {'id': this.id, 'class': cssClass}, this.workspace.getCanvas(), ); @@ -179,7 +186,7 @@ export class FlyoutButton implements IASTNodeLocationSvg { fontWeight, fontFamily, ); - this.height = fontMetrics.height; + this.height = this.height || fontMetrics.height; if (!this.isFlyoutLabel) { this.width += 2 * FlyoutButton.TEXT_MARGIN_X; @@ -198,15 +205,24 @@ export class FlyoutButton implements IASTNodeLocationSvg { this.updateTransform(); - // AnyDuringMigration because: Argument of type 'SVGGElement | null' is not - // assignable to parameter of type 'EventTarget'. + this.onMouseDownWrapper = browserEvents.conditionalBind( + this.svgGroup, + 'pointerdown', + this, + this.onMouseDown, + ); this.onMouseUpWrapper = browserEvents.conditionalBind( - this.svgGroup as AnyDuringMigration, + this.svgGroup, 'pointerup', this, this.onMouseUp, ); - return this.svgGroup!; + } + + createDom(): SVGElement { + // No-op, now handled in constructor. Will be removed in followup refactor + // PR that updates the flyout classes to use inflaters. + return this.svgGroup; } /** Correctly position the flyout button and make it visible. */ @@ -235,6 +251,17 @@ export class FlyoutButton implements IASTNodeLocationSvg { this.updateTransform(); } + /** + * 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[]) { + this.moveTo(this.position.x + dx, this.position.y + dy); + } + /** @returns Whether or not the button is a label. */ isLabel(): boolean { return this.isFlyoutLabel; @@ -250,6 +277,21 @@ export class FlyoutButton implements IASTNodeLocationSvg { return this.position; } + /** + * 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() { + return new Rect( + this.position.y, + this.position.y + this.height, + this.position.x, + this.position.x + this.width, + ); + } + /** @returns Text of the button. */ getButtonText(): string { return this.text; @@ -275,9 +317,8 @@ export class FlyoutButton implements IASTNodeLocationSvg { /** Dispose of this button. */ dispose() { - if (this.onMouseUpWrapper) { - browserEvents.unbind(this.onMouseUpWrapper); - } + browserEvents.unbind(this.onMouseDownWrapper); + browserEvents.unbind(this.onMouseUpWrapper); if (this.svgGroup) { dom.removeNode(this.svgGroup); } @@ -303,15 +344,6 @@ export class FlyoutButton implements IASTNodeLocationSvg { } } - /** - * 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. * @@ -342,6 +374,46 @@ export class FlyoutButton implements IASTNodeLocationSvg { } } } + + private onMouseDown(e: PointerEvent) { + const gesture = this.targetWorkspace.getGesture(e); + const flyout = this.targetWorkspace.getFlyout(); + if (gesture && flyout) { + gesture.handleFlyoutStart(e, flyout); + } + } + + /** + * @returns The root SVG element of this rendered element. + */ + getSvgRoot() { + return this.svgGroup; + } + + /** See IFocusableNode.getFocusableElement. */ + getFocusableElement(): HTMLElement | SVGElement { + return this.svgGroup; + } + + /** See IFocusableNode.getFocusableTree. */ + getFocusableTree(): IFocusableTree { + return this.workspace; + } + + /** See IFocusableNode.onNodeFocus. */ + onNodeFocus(): void { + const xy = this.getPosition(); + const bounds = new Rect(xy.y, xy.y + this.height, xy.x, xy.x + this.width); + this.workspace.scrollBoundsIntoView(bounds); + } + + /** See IFocusableNode.onNodeBlur. */ + onNodeBlur(): void {} + + /** See IFocusableNode.canBeFocused. */ + canBeFocused(): boolean { + return true; + } } /** CSS for buttons and labels. See css.js for use. */ @@ -355,7 +427,13 @@ Css.register(` fill: #666; } -.blocklyFlyoutButton:hover { +@media (hover: hover) { + .blocklyFlyoutButton:hover { + fill: #aaa; + } +} + +.blocklyFlyoutButton:active { fill: #aaa; } diff --git a/core/flyout_horizontal.ts b/packages/blockly/core/flyout_horizontal.ts similarity index 81% rename from core/flyout_horizontal.ts rename to packages/blockly/core/flyout_horizontal.ts index 6e77636e86b..47b7ab06abd 100644 --- a/core/flyout_horizontal.ts +++ b/packages/blockly/core/flyout_horizontal.ts @@ -13,8 +13,8 @@ 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 {Flyout} from './flyout_base.js'; +import type {FlyoutItem} from './flyout_item.js'; import type {Options} from './options.js'; import * as registry from './registry.js'; import {Scrollbar} from './scrollbar.js'; @@ -98,7 +98,7 @@ export class HorizontalFlyout extends Flyout { if (atTop) { y = toolboxMetrics.height; } else { - y = viewMetrics.height - this.height_; + y = viewMetrics.height - this.getHeight(); } } else { if (atTop) { @@ -116,7 +116,7 @@ export class HorizontalFlyout extends Flyout { // 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_; + y = viewMetrics.height + absoluteMetrics.top - this.getHeight(); } } @@ -133,13 +133,13 @@ export class HorizontalFlyout extends Flyout { this.width_ = targetWorkspaceViewMetrics.width; const edgeWidth = targetWorkspaceViewMetrics.width - 2 * this.CORNER_RADIUS; - const edgeHeight = this.height_ - this.CORNER_RADIUS; + const edgeHeight = this.getHeight() - this.CORNER_RADIUS; this.setBackgroundPath(edgeWidth, edgeHeight); const x = this.getX(); const y = this.getY(); - this.positionAt_(this.width_, this.height_, x, y); + this.positionAt_(this.getWidth(), this.getHeight(), x, y); } /** @@ -252,10 +252,9 @@ export class HorizontalFlyout extends Flyout { /** * Lay out the blocks in the flyout. * - * @param contents The blocks and buttons to lay out. - * @param gaps The visible gaps between blocks. + * @param contents The flyout items to lay out. */ - protected override layout_(contents: FlyoutItem[], gaps: number[]) { + protected override layout_(contents: FlyoutItem[]) { this.workspace_.scale = this.targetWorkspace!.scale; const margin = this.MARGIN; let cursorX = margin + this.tabWidth_; @@ -264,43 +263,11 @@ export class HorizontalFlyout extends Flyout { 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]; - } + for (const item of contents) { + const rect = item.getElement().getBoundingRectangle(); + const moveX = this.RTL ? cursorX + rect.getWidth() : cursorX; + item.getElement().moveBy(moveX, cursorY); + cursorX += item.getElement().getBoundingRectangle().getWidth(); } } @@ -367,26 +334,17 @@ export class HorizontalFlyout extends Flyout { */ 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); - } + let flyoutHeight = this.getContents().reduce((maxHeightSoFar, item) => { + return Math.max( + maxHeightSoFar, + item.getElement().getBoundingRectangle().getHeight(), + ); + }, 0); 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); - } - } - + if (this.getHeight() !== flyoutHeight) { // TODO(#7689): Remove this. // Workspace with no scrollbars where this is permanently open on the top. // If scrollbars exist they properly update the metrics. diff --git a/packages/blockly/core/flyout_item.ts b/packages/blockly/core/flyout_item.ts new file mode 100644 index 00000000000..26be0ed12e2 --- /dev/null +++ b/packages/blockly/core/flyout_item.ts @@ -0,0 +1,33 @@ +import type {IBoundedElement} from './interfaces/i_bounded_element.js'; +import type {IFocusableNode} from './interfaces/i_focusable_node.js'; + +/** + * Representation of an item displayed in a flyout. + */ +export class FlyoutItem { + /** + * Creates a new FlyoutItem. + * + * @param element The element that will be displayed in the flyout. + * @param type The type of element. Should correspond to the type of the + * flyout inflater that created this object. + */ + constructor( + private element: IBoundedElement & IFocusableNode, + private type: string, + ) {} + + /** + * Returns the element displayed in the flyout. + */ + getElement() { + return this.element; + } + + /** + * Returns the type of flyout element this item represents. + */ + getType() { + return this.type; + } +} diff --git a/core/flyout_metrics_manager.ts b/packages/blockly/core/flyout_metrics_manager.ts similarity index 100% rename from core/flyout_metrics_manager.ts rename to packages/blockly/core/flyout_metrics_manager.ts diff --git a/packages/blockly/core/flyout_navigator.ts b/packages/blockly/core/flyout_navigator.ts new file mode 100644 index 00000000000..a102ce81765 --- /dev/null +++ b/packages/blockly/core/flyout_navigator.ts @@ -0,0 +1,24 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {IFlyout} from './interfaces/i_flyout.js'; +import {FlyoutButtonNavigationPolicy} from './keyboard_nav/flyout_button_navigation_policy.js'; +import {FlyoutNavigationPolicy} from './keyboard_nav/flyout_navigation_policy.js'; +import {FlyoutSeparatorNavigationPolicy} from './keyboard_nav/flyout_separator_navigation_policy.js'; +import {Navigator} from './navigator.js'; + +export class FlyoutNavigator extends Navigator { + constructor(flyout: IFlyout) { + super(); + this.rules.push( + new FlyoutButtonNavigationPolicy(), + new FlyoutSeparatorNavigationPolicy(), + ); + this.rules = this.rules.map( + (rule) => new FlyoutNavigationPolicy(rule, flyout), + ); + } +} diff --git a/packages/blockly/core/flyout_separator.ts b/packages/blockly/core/flyout_separator.ts new file mode 100644 index 00000000000..e9ace428ec9 --- /dev/null +++ b/packages/blockly/core/flyout_separator.ts @@ -0,0 +1,94 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {IBoundedElement} from './interfaces/i_bounded_element.js'; +import type {IFocusableNode} from './interfaces/i_focusable_node.js'; +import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; +import {Rect} from './utils/rect.js'; + +/** + * Representation of a gap between elements in a flyout. + */ +export class FlyoutSeparator implements IBoundedElement, IFocusableNode { + private x = 0; + private y = 0; + + /** + * Creates a new separator. + * + * @param gap The amount of space this separator should occupy. + * @param axis The axis along which this separator occupies space. + */ + constructor( + private gap: number, + private axis: SeparatorAxis, + ) {} + + /** + * Returns the bounding box of this separator. + * + * @returns The bounding box of this separator. + */ + getBoundingRectangle(): Rect { + switch (this.axis) { + case SeparatorAxis.X: + return new Rect(this.y, this.y, this.x, this.x + this.gap); + case SeparatorAxis.Y: + return new Rect(this.y, this.y + this.gap, this.x, this.x); + } + } + + /** + * Repositions this separator. + * + * @param dx The distance to move this separator on the X axis. + * @param dy The distance to move this separator on the Y axis. + * @param _reason The reason this move was initiated. + */ + moveBy(dx: number, dy: number, _reason?: string[]) { + this.x += dx; + this.y += dy; + } + + /** + * Returns false to prevent this separator from being navigated to by the + * keyboard. + * + * @returns False. + */ + isNavigable() { + return false; + } + + /** See IFocusableNode.getFocusableElement. */ + getFocusableElement(): HTMLElement | SVGElement { + throw new Error('Cannot be focused'); + } + + /** See IFocusableNode.getFocusableTree. */ + getFocusableTree(): IFocusableTree { + throw new Error('Cannot be focused'); + } + + /** See IFocusableNode.onNodeFocus. */ + onNodeFocus(): void {} + + /** See IFocusableNode.onNodeBlur. */ + onNodeBlur(): void {} + + /** See IFocusableNode.canBeFocused. */ + canBeFocused(): boolean { + return false; + } +} + +/** + * Representation of an axis along which a separator occupies space. + */ +export const enum SeparatorAxis { + X = 'x', + Y = 'y', +} diff --git a/core/flyout_vertical.ts b/packages/blockly/core/flyout_vertical.ts similarity index 76% rename from core/flyout_vertical.ts rename to packages/blockly/core/flyout_vertical.ts index 59682a390d2..968b7c02458 100644 --- a/core/flyout_vertical.ts +++ b/packages/blockly/core/flyout_vertical.ts @@ -13,8 +13,8 @@ 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 {Flyout} from './flyout_base.js'; +import type {FlyoutItem} from './flyout_item.js'; import type {Options} from './options.js'; import * as registry from './registry.js'; import {Scrollbar} from './scrollbar.js'; @@ -86,7 +86,7 @@ export class VerticalFlyout extends Flyout { if (this.toolboxPosition_ === toolbox.Position.LEFT) { x = toolboxMetrics.width; } else { - x = viewMetrics.width - this.width_; + x = viewMetrics.width - this.getWidth(); } } else { if (this.toolboxPosition_ === toolbox.Position.LEFT) { @@ -104,7 +104,7 @@ export class VerticalFlyout extends Flyout { // 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_; + x = viewMetrics.width + absoluteMetrics.left - this.getWidth(); } } @@ -130,7 +130,7 @@ export class VerticalFlyout extends Flyout { const targetWorkspaceViewMetrics = metricsManager.getViewMetrics(); this.height_ = targetWorkspaceViewMetrics.height; - const edgeWidth = this.width_ - this.CORNER_RADIUS; + const edgeWidth = this.getWidth() - this.CORNER_RADIUS; const edgeHeight = targetWorkspaceViewMetrics.height - 2 * this.CORNER_RADIUS; this.setBackgroundPath(edgeWidth, edgeHeight); @@ -138,7 +138,7 @@ export class VerticalFlyout extends Flyout { const x = this.getX(); const y = this.getY(); - this.positionAt_(this.width_, this.height_, x, y); + this.positionAt_(this.getWidth(), this.getHeight(), x, y); } /** @@ -221,51 +221,17 @@ export class VerticalFlyout extends Flyout { /** * Lay out the blocks in the flyout. * - * @param contents The blocks and buttons to lay out. - * @param gaps The visible gaps between blocks. + * @param contents The flyout items to lay out. */ - protected override layout_(contents: FlyoutItem[], gaps: number[]) { + protected override layout_(contents: FlyoutItem[]) { 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]; - } + for (const item of contents) { + item.getElement().moveBy(cursorX, cursorY); + cursorY += item.getElement().getBoundingRectangle().getHeight(); } } @@ -328,52 +294,32 @@ export class VerticalFlyout extends Flyout { } /** - * Compute width of flyout. toolbox.Position mat under each block. + * Compute width of flyout. * 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); - } + let flyoutWidth = this.getContents().reduce((maxWidthSoFar, item) => { + return Math.max( + maxWidthSoFar, + item.getElement().getBoundingRectangle().getWidth(), + ); + }, 0); 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.getWidth() !== flyoutWidth) { 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 = + // With the flyoutWidth known, right-align the flyout contents. + for (const item of this.getContents()) { + const oldX = item.getElement().getBoundingRectangle().left; + const newX = flyoutWidth / this.workspace_.scale - - button.width - + item.getElement().getBoundingRectangle().getWidth() - this.MARGIN - this.tabWidth_; - button.moveTo(x, y); + item.getElement().moveBy(newX - oldX, 0); } } diff --git a/packages/blockly/core/focus_manager.ts b/packages/blockly/core/focus_manager.ts new file mode 100644 index 00000000000..47e4324540d --- /dev/null +++ b/packages/blockly/core/focus_manager.ts @@ -0,0 +1,675 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {IFocusableNode} from './interfaces/i_focusable_node.js'; +import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; +import * as dom from './utils/dom.js'; +import {FocusableTreeTraverser} from './utils/focusable_tree_traverser.js'; + +/** + * Type declaration for returning focus to FocusManager upon completing an + * ephemeral UI flow (such as a dialog). + * + * See FocusManager.takeEphemeralFocus for more details. + */ +export type ReturnEphemeralFocus = () => void; + +/** + * Represents an IFocusableTree that has been registered for focus management in + * FocusManager. + */ +class TreeRegistration { + /** + * Constructs a new TreeRegistration. + * + * @param tree The tree being registered. + * @param rootShouldBeAutoTabbable Whether the tree should have automatic + * top-level tab management. + */ + constructor( + readonly tree: IFocusableTree, + readonly rootShouldBeAutoTabbable: boolean, + ) {} +} + +/** + * A per-page singleton that manages Blockly focus across one or more + * IFocusableTrees, and bidirectionally synchronizes this focus with the DOM. + * + * Callers that wish to explicitly change input focus for select Blockly + * components on the page should use the focus functions in this manager. + * + * The manager is responsible for handling focus events from the DOM (which may + * may arise from users clicking on page elements) and ensuring that + * corresponding IFocusableNodes are clearly marked as actively/passively + * highlighted in the same way that this would be represented with calls to + * focusNode(). + */ +export class FocusManager { + /** + * The CSS class assigned to IFocusableNode elements that presently have + * active DOM and Blockly focus. + * + * This should never be used directly. Instead, rely on FocusManager to ensure + * nodes have active focus (either automatically through DOM focus or manually + * through the various focus* methods provided by this class). + * + * It's recommended to not query using this class name, either. Instead, use + * FocusableTreeTraverser or IFocusableTree's methods to find a specific node. + */ + static readonly ACTIVE_FOCUS_NODE_CSS_CLASS_NAME = 'blocklyActiveFocus'; + + /** + * The CSS class assigned to IFocusableNode elements that presently have + * passive focus (that is, they were the most recent node in their relative + * tree to have active focus--see ACTIVE_FOCUS_NODE_CSS_CLASS_NAME--and will + * receive active focus again if their surrounding tree is requested to become + * focused, i.e. using focusTree below). + * + * See ACTIVE_FOCUS_NODE_CSS_CLASS_NAME for caveats and limitations around + * using this constant directly (generally it never should need to be used). + */ + static readonly PASSIVE_FOCUS_NODE_CSS_CLASS_NAME = 'blocklyPassiveFocus'; + + private focusedNode: IFocusableNode | null = null; + private previouslyFocusedNode: IFocusableNode | null = null; + private registeredTrees: Array = []; + + private currentlyHoldsEphemeralFocus: boolean = false; + private lockFocusStateChanges: boolean = false; + private recentlyLostAllFocus: boolean = false; + private isUpdatingFocusedNode: boolean = false; + + constructor( + addGlobalEventListener: (type: string, listener: EventListener) => void, + ) { + // Note that 'element' here is the element *gaining* focus. + const maybeFocus = (element: Element | EventTarget | null) => { + // Skip processing the event if the focused node is currently updating. + if (this.isUpdatingFocusedNode) return; + + this.recentlyLostAllFocus = !element; + let newNode: IFocusableNode | null | undefined = null; + if (element instanceof HTMLElement || element instanceof SVGElement) { + // If the target losing or gaining focus maps to any tree, then it + // should be updated. Per the contract of findFocusableNodeFor only one + // tree should claim the element, so the search can be exited early. + for (const reg of this.registeredTrees) { + const tree = reg.tree; + newNode = FocusableTreeTraverser.findFocusableNodeFor(element, tree); + if (newNode) break; + } + } + + if (newNode && newNode.canBeFocused()) { + const newTree = newNode.getFocusableTree(); + const oldTree = this.focusedNode?.getFocusableTree(); + if (newNode === newTree.getRootFocusableNode() && newTree !== oldTree) { + // If the root of the tree is the one taking focus (such as due to + // being tabbed), try to focus the whole tree explicitly to ensure the + // correct node re-receives focus. + this.focusTree(newTree); + } else { + this.focusNode(newNode); + } + } else { + this.defocusCurrentFocusedNode(); + } + }; + + // Register root document focus listeners for tracking when focus leaves all + // tracked focusable trees. Note that focusin and focusout can be somewhat + // overlapping in the information that they provide. This is fine because + // they both aim to check for focus changes on the element gaining or having + // received focus, and maybeFocus should behave relatively deterministic. + addGlobalEventListener('focusin', (event) => { + if (!(event instanceof FocusEvent)) return; + + // When something receives focus, always use the current active element as + // the source of truth. + maybeFocus(document.activeElement); + }); + addGlobalEventListener('focusout', (event) => { + if (!(event instanceof FocusEvent)) return; + + // When something loses focus, it seems that document.activeElement may + // not necessarily be correct. Instead, use relatedTarget. + maybeFocus(event.relatedTarget); + }); + } + + /** + * Registers a new IFocusableTree for automatic focus management. + * + * If the tree currently has an element with DOM focus, it will not affect the + * internal state in this manager until the focus changes to a new, + * now-monitored element/node. + * + * This function throws if the provided tree is already currently registered + * in this manager. Use isRegistered to check in cases when it can't be + * certain whether the tree has been registered. + * + * The tree's registration can be customized to configure automatic tab stops. + * This specifically provides capability for the user to be able to tab + * navigate to the root of the tree but only when the tree doesn't hold active + * focus. If this functionality is disabled then the tree's root will + * automatically be made focusable (but not tabbable) when it is first focused + * in the same way as any other focusable node. + * + * @param tree The IFocusableTree to register. + * @param rootShouldBeAutoTabbable Whether the root of this tree should be + * added as a top-level page tab stop when it doesn't hold active focus. + */ + registerTree( + tree: IFocusableTree, + rootShouldBeAutoTabbable: boolean = false, + ): void { + this.ensureManagerIsUnlocked(); + if (this.isRegistered(tree)) { + throw Error(`Attempted to re-register already registered tree: ${tree}.`); + } + this.registeredTrees.push( + new TreeRegistration(tree, rootShouldBeAutoTabbable), + ); + const rootElement = tree.getRootFocusableNode().getFocusableElement(); + if (!rootElement.id || rootElement.id === 'null') { + throw Error( + `Attempting to register a tree with a root element that has an ` + + `invalid ID: ${tree}.`, + ); + } + if (rootShouldBeAutoTabbable) { + rootElement.tabIndex = 0; + } + } + + /** + * Returns whether the specified tree has already been registered in this + * manager using registerTree and hasn't yet been unregistered using + * unregisterTree. + */ + isRegistered(tree: IFocusableTree): boolean { + return !!this.lookUpRegistration(tree); + } + + /** + * Returns the TreeRegistration for the specified tree, or null if the tree is + * not currently registered. + */ + private lookUpRegistration(tree: IFocusableTree): TreeRegistration | null { + return this.registeredTrees.find((reg) => reg.tree === tree) ?? null; + } + + /** + * Unregisters a IFocusableTree from automatic focus management. + * + * If the tree had a previous focused node, it will have its highlight + * removed. This function does NOT change DOM focus. + * + * This function throws if the provided tree is not currently registered in + * this manager. + * + * This function will reset the tree's root element tabindex if the tree was + * registered with automatic tab management. + */ + unregisterTree(tree: IFocusableTree): void { + this.ensureManagerIsUnlocked(); + if (!this.isRegistered(tree)) { + throw Error(`Attempted to unregister not registered tree: ${tree}.`); + } + const treeIndex = this.registeredTrees.findIndex( + (reg) => reg.tree === tree, + ); + const registration = this.registeredTrees[treeIndex]; + this.registeredTrees.splice(treeIndex, 1); + + const focusedNode = FocusableTreeTraverser.findFocusedNode(tree); + const root = tree.getRootFocusableNode(); + if (focusedNode) this.removeHighlight(focusedNode); + if (this.focusedNode === focusedNode || this.focusedNode === root) { + this.updateFocusedNode(null); + } + this.removeHighlight(root); + + if (registration.rootShouldBeAutoTabbable) { + tree + .getRootFocusableNode() + .getFocusableElement() + .removeAttribute('tabindex'); + } + } + + /** + * Returns the current IFocusableTree that has focus, or null if none + * currently do. + * + * Note also that if ephemeral focus is currently captured (e.g. using + * takeEphemeralFocus) then the returned tree here may not currently have DOM + * focus. + */ + getFocusedTree(): IFocusableTree | null { + return this.focusedNode?.getFocusableTree() ?? null; + } + + /** + * Returns the current IFocusableNode with focus (which is always tied to a + * focused IFocusableTree), or null if there isn't one. + * + * Note that this function will maintain parity with + * IFocusableTree.getFocusedNode(). That is, if a tree itself has focus but + * none of its non-root children do, this will return null but + * getFocusedTree() will not. + * + * Note also that if ephemeral focus is currently captured (e.g. using + * takeEphemeralFocus) then the returned node here may not currently have DOM + * focus. + */ + getFocusedNode(): IFocusableNode | null { + return this.focusedNode; + } + + /** + * Focuses the specific IFocusableTree. This either means restoring active + * focus to the tree's passively focused node, or focusing the tree's root + * node. + * + * Note that if the specified tree already has a focused node then this will + * not change any existing focus (unless that node has passive focus, then it + * will be restored to active focus). + * + * See getFocusedNode for details on how other nodes are affected. + * + * @param focusableTree The tree that should receive active + * focus. + */ + focusTree(focusableTree: IFocusableTree): void { + this.ensureManagerIsUnlocked(); + if (!this.isRegistered(focusableTree)) { + throw Error(`Attempted to focus unregistered tree: ${focusableTree}.`); + } + const currNode = FocusableTreeTraverser.findFocusedNode(focusableTree); + const nodeToRestore = focusableTree.getRestoredFocusableNode(currNode); + const rootFallback = focusableTree.getRootFocusableNode(); + this.focusNode(nodeToRestore ?? currNode ?? rootFallback); + } + + /** + * Focuses DOM input on the specified node, and marks it as actively focused. + * + * Any previously focused node will be updated to be passively highlighted (if + * it's in a different focusable tree) or blurred (if it's in the same one). + * + * **Important**: If the provided node is not able to be focused (e.g. its + * canBeFocused() method returns false), it will be ignored and any existing + * focus state will remain unchanged. + * + * Note that this may update the specified node's element's tabindex to ensure + * that it can be properly read out by screenreaders while focused. + * + * The focused node will not be automatically scrolled into view. + * + * @param focusableNode The node that should receive active focus. + */ + focusNode(focusableNode: IFocusableNode): void { + this.ensureManagerIsUnlocked(); + const mustRestoreUpdatingNode = !this.currentlyHoldsEphemeralFocus; + if (mustRestoreUpdatingNode) { + // Disable state syncing from DOM events since possible calls to focus() + // below will loop a call back to focusNode(). + this.isUpdatingFocusedNode = true; + } + + // Double check that state wasn't desynchronized in the background. See: + // https://github.com/google/blockly-keyboard-experimentation/issues/87. + // This is only done for the case where the same node is being focused twice + // since other cases should automatically correct (due to the rest of the + // routine running as normal). + const prevFocusedElement = this.focusedNode?.getFocusableElement(); + const hasDesyncedState = prevFocusedElement !== document.activeElement; + if (this.focusedNode === focusableNode && !hasDesyncedState) { + if (mustRestoreUpdatingNode) { + // Reenable state syncing from DOM events. + this.isUpdatingFocusedNode = false; + } + return; // State is unchanged. + } + + if (!focusableNode.canBeFocused()) { + // This node can't be focused. + console.warn("Trying to focus a node that can't be focused."); + + if (mustRestoreUpdatingNode) { + // Reenable state syncing from DOM events. + this.isUpdatingFocusedNode = false; + } + return; + } + + const nextTree = focusableNode.getFocusableTree(); + if (!this.isRegistered(nextTree)) { + throw Error(`Attempted to focus unregistered node: ${focusableNode}.`); + } + + const focusableNodeElement = focusableNode.getFocusableElement(); + if (!focusableNodeElement.id || focusableNodeElement.id === 'null') { + // Warn that the ID is invalid, but continue execution since an invalid ID + // will result in an unmatched (null) node. Since a request to focus + // something was initiated, the code below will attempt to find the next + // best thing to focus, instead. + console.warn('Trying to focus a node that has an invalid ID.'); + } + + // Safety check for ensuring focusNode() doesn't get called for a node that + // isn't actually hooked up to its parent tree correctly. This usually + // happens when calls to focusNode() interleave with asynchronous clean-up + // operations (which can happen due to ephemeral focus and in other cases). + // Fall back to a reasonable default since there's no valid node to focus. + const matchedNode = FocusableTreeTraverser.findFocusableNodeFor( + focusableNodeElement, + nextTree, + ); + const prevNodeNextTree = FocusableTreeTraverser.findFocusedNode(nextTree); + let nodeToFocus = focusableNode; + if (matchedNode !== focusableNode) { + const nodeToRestore = nextTree.getRestoredFocusableNode(prevNodeNextTree); + const rootFallback = nextTree.getRootFocusableNode(); + nodeToFocus = nodeToRestore ?? prevNodeNextTree ?? rootFallback; + } + + const prevNode = this.focusedNode; + const prevTree = prevNode?.getFocusableTree(); + if (prevNode) { + this.passivelyFocusNode(prevNode, nextTree); + } + + // If there's a focused node in the new node's tree, ensure it's reset. + const nextTreeRoot = nextTree.getRootFocusableNode(); + if (prevNodeNextTree) { + this.removeHighlight(prevNodeNextTree); + } + // For caution, ensure that the root is always reset since getFocusedNode() + // is expected to return null if the root was highlighted, if the root is + // not the node now being set to active. + if (nextTreeRoot !== nodeToFocus) { + this.removeHighlight(nextTreeRoot); + } + + if (!this.currentlyHoldsEphemeralFocus) { + // Only change the actively focused node if ephemeral state isn't held. + this.activelyFocusNode(nodeToFocus, prevTree ?? null); + } + this.updateFocusedNode(nodeToFocus); + if (mustRestoreUpdatingNode) { + // Reenable state syncing from DOM events. + this.isUpdatingFocusedNode = false; + } + } + + /** + * Ephemerally captures focus for a specific element until the returned lambda + * is called. This is expected to be especially useful for ephemeral UI flows + * like dialogs. + * + * IMPORTANT: the returned lambda *must* be called, otherwise automatic focus + * will no longer work anywhere on the page. It is highly recommended to tie + * the lambda call to the closure of the corresponding UI so that if input is + * manually changed to an element outside of the ephemeral UI, the UI should + * close and automatic input restored. Note that this lambda must be called + * exactly once and that subsequent calls will throw an error. + * + * Note that the manager will continue to track DOM input signals even when + * ephemeral focus is active, but it won't actually change node state until + * the returned lambda is called. Additionally, only 1 ephemeral focus context + * can be active at any given time (attempting to activate more than one + * simultaneously will result in an error being thrown). + * + * This method does not scroll the ephemerally focused element into view. + */ + takeEphemeralFocus( + focusableElement: HTMLElement | SVGElement, + ): ReturnEphemeralFocus { + this.ensureManagerIsUnlocked(); + if (this.currentlyHoldsEphemeralFocus) { + throw Error( + `Attempted to take ephemeral focus when it's already held, ` + + `with new element: ${focusableElement}.`, + ); + } + this.currentlyHoldsEphemeralFocus = true; + + if (this.focusedNode) { + this.passivelyFocusNode(this.focusedNode, null); + } + focusableElement.focus({preventScroll: true}); + + let hasFinishedEphemeralFocus = false; + return () => { + if (hasFinishedEphemeralFocus) { + throw Error( + `Attempted to finish ephemeral focus twice for element: ` + + `${focusableElement}.`, + ); + } + hasFinishedEphemeralFocus = true; + this.currentlyHoldsEphemeralFocus = false; + + if (this.focusedNode) { + this.activelyFocusNode(this.focusedNode, null); + + // Even though focus was restored, check if it's lost again. It's + // possible for the browser to force focus away from all elements once + // the ephemeral element disappears. This ensures focus is restored. + const capturedNode = this.focusedNode; + setTimeout(() => { + // These checks are set up to minimize the risk that a legitimate + // focus change occurred within the delay that this would override. + if ( + !this.focusedNode && + this.previouslyFocusedNode === capturedNode && + this.recentlyLostAllFocus + ) { + this.focusNode(capturedNode); + } + }, 0); + } + }; + } + + /** + * @returns whether something is currently holding ephemeral focus + */ + ephemeralFocusTaken(): boolean { + return this.currentlyHoldsEphemeralFocus; + } + + /** + * Ensures that the manager is currently allowing operations that change its + * internal focus state (such as via focusNode()). + * + * If the manager is currently not allowing state changes, an exception is + * thrown. + */ + private ensureManagerIsUnlocked(): void { + if (this.lockFocusStateChanges) { + throw Error( + 'FocusManager state changes cannot happen in a tree/node focus/blur ' + + 'callback.', + ); + } + } + + /** + * Updates the internally tracked focused node to the specified node, or null + * if focus is being lost. This also updates previous focus tracking. + * + * @param newFocusedNode The new node to set as focused. + */ + private updateFocusedNode(newFocusedNode: IFocusableNode | null) { + this.previouslyFocusedNode = this.focusedNode; + this.focusedNode = newFocusedNode; + } + + /** + * Defocuses the current actively focused node tracked by the manager, iff + * there's a node being tracked and the manager doesn't have ephemeral focus. + */ + private defocusCurrentFocusedNode(): void { + // The current node will likely be defocused while ephemeral focus is held, + // but internal manager state shouldn't change since the node should be + // restored upon exiting ephemeral focus mode. + if (this.focusedNode && !this.currentlyHoldsEphemeralFocus) { + this.passivelyFocusNode(this.focusedNode, null); + this.updateFocusedNode(null); + } + } + + /** + * Marks the specified node as actively focused, also calling related + * lifecycle callback methods for both the node and its parent tree. This + * ensures that the node is properly styled to indicate its active focus. + * + * This does not change the manager's currently tracked node, nor does it + * change any other nodes. + * + * @param node The node to be actively focused. + * @param prevTree The tree of the previously actively focused node, or null + * if there wasn't a previously actively focused node. + */ + private activelyFocusNode( + node: IFocusableNode, + prevTree: IFocusableTree | null, + ): void { + // Note that order matters here. Focus callbacks are allowed to change + // element visibility which can influence focusability, including for a + // node's focusable element (which *is* allowed to be invisible until the + // node needs to be focused). + this.lockFocusStateChanges = true; + const tree = node.getFocusableTree(); + const elem = node.getFocusableElement(); + const nextTreeReg = this.lookUpRegistration(tree); + const treeIsTabManaged = nextTreeReg?.rootShouldBeAutoTabbable; + if (tree !== prevTree) { + tree.onTreeFocus(node, prevTree); + + if (treeIsTabManaged) { + // If this node's tree has its tab auto-managed, ensure that it's no + // longer tabbable now that it holds active focus. + tree.getRootFocusableNode().getFocusableElement().tabIndex = -1; + } + } + node.onNodeFocus(); + this.lockFocusStateChanges = false; + + // The tab index should be set in all cases where: + // - It doesn't overwrite an pre-set tab index for the node. + // - The node is part of a tree whose tab index is unmanaged. + // OR + // - The node is part of a managed tree but this isn't the root. Managed + // roots are ignored since they are always overwritten to have a tab index + // of -1 with active focus so that they cannot be tab navigated. + // + // Setting the tab index ensures that the node's focusable element can + // actually receive DOM focus. + if (!treeIsTabManaged || node !== tree.getRootFocusableNode()) { + if (!elem.hasAttribute('tabindex')) elem.tabIndex = -1; + } + + this.setNodeToVisualActiveFocus(node); + elem.focus({preventScroll: true}); + } + + /** + * Marks the specified node as passively focused, also calling related + * lifecycle callback methods for both the node and its parent tree. This + * ensures that the node is properly styled to indicate its passive focus. + * + * This does not change the manager's currently tracked node, nor does it + * change any other nodes. + * + * @param node The node to be passively focused. + * @param nextTree The tree of the node receiving active focus, or null if no + * node will be actively focused. + */ + private passivelyFocusNode( + node: IFocusableNode, + nextTree: IFocusableTree | null, + ): void { + this.lockFocusStateChanges = true; + const tree = node.getFocusableTree(); + if (tree !== nextTree) { + tree.onTreeBlur(nextTree); + + const reg = this.lookUpRegistration(tree); + if (reg?.rootShouldBeAutoTabbable) { + // If this node's tree has its tab auto-managed, ensure that it's now + // tabbable since it no longer holds active focus. + tree.getRootFocusableNode().getFocusableElement().tabIndex = 0; + } + } + node.onNodeBlur(); + this.lockFocusStateChanges = false; + + if (tree !== nextTree) { + this.setNodeToVisualPassiveFocus(node); + } + } + + /** + * Updates the node's styling to indicate that it should have an active focus + * indicator. + * + * @param node The node to be styled for active focus. + */ + private setNodeToVisualActiveFocus(node: IFocusableNode): void { + const element = node.getFocusableElement(); + dom.addClass(element, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + dom.removeClass(element, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + } + + /** + * Updates the node's styling to indicate that it should have a passive focus + * indicator. + * + * @param node The node to be styled for passive focus. + */ + private setNodeToVisualPassiveFocus(node: IFocusableNode): void { + const element = node.getFocusableElement(); + dom.removeClass(element, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + dom.addClass(element, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + } + + /** + * Removes any active/passive indicators for the specified node. + * + * @param node The node which should have neither passive nor active focus + * indication. + */ + private removeHighlight(node: IFocusableNode): void { + const element = node.getFocusableElement(); + dom.removeClass(element, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + dom.removeClass(element, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + } + + private static focusManager: FocusManager | null = null; + + /** + * Returns the page-global FocusManager. + * + * The returned instance is guaranteed to not change across function calls, + * but may change across page loads. + */ + static getFocusManager(): FocusManager { + if (!FocusManager.focusManager) { + FocusManager.focusManager = new FocusManager(document.addEventListener); + } + return FocusManager.focusManager; + } +} + +/** Convenience function for FocusManager.getFocusManager. */ +export function getFocusManager(): FocusManager { + return FocusManager.getFocusManager(); +} diff --git a/core/generator.ts b/packages/blockly/core/generator.ts similarity index 99% rename from core/generator.ts rename to packages/blockly/core/generator.ts index 5884b4e5449..24510fd5b3a 100644 --- a/core/generator.ts +++ b/packages/blockly/core/generator.ts @@ -252,8 +252,7 @@ export class CodeGenerator { 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. + // Look up block generator function in dictionary. const func = this.forBlock[block.type]; if (typeof func !== 'function') { throw Error( diff --git a/core/gesture.ts b/packages/blockly/core/gesture.ts similarity index 93% rename from core/gesture.ts rename to packages/blockly/core/gesture.ts index 0b65299e578..9d617c4c62b 100644 --- a/core/gesture.ts +++ b/packages/blockly/core/gesture.ts @@ -25,11 +25,14 @@ 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 {getFocusManager} from './focus_manager.js'; import type {IBubble} from './interfaces/i_bubble.js'; +import {hasContextMenu} from './interfaces/i_contextmenu.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 {keyboardNavigationController} from './keyboard_navigation_controller.js'; import * as registry from './registry.js'; import * as Tooltip from './tooltip.js'; import * as Touch from './touch.js'; @@ -206,10 +209,6 @@ export class Gesture { browserEvents.unbind(event); } this.boundEvents.length = 0; - - if (this.workspaceDragger) { - this.workspaceDragger.dispose(); - } } /** @@ -275,24 +274,19 @@ export class Gesture { 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; + + 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); } - return false; + // The start block is no longer relevant, because this is a drag. + this.startBlock = null; + this.targetBlock = this.flyout.createBlock(this.targetBlock); + getFocusManager().focusNode(this.targetBlock); + return true; } /** @@ -465,6 +459,15 @@ export class Gesture { /* opt_noCaptureIdentifier */ true, ), ); + this.boundEvents.push( + browserEvents.conditionalBind( + document, + 'pointercancel', + null, + this.handleUp.bind(this), + /* opt_noCaptureIdentifier */ true, + ), + ); e.preventDefault(); e.stopPropagation(); @@ -540,8 +543,10 @@ export class Gesture { // have higher priority than workspaces. The ordering within drags does // not matter, because the three types of dragging are exclusive. if (this.dragger) { + keyboardNavigationController.setIsActive(false); this.dragger.onDragEnd(e, this.currentDragDeltaXY); } else if (this.workspaceDragger) { + keyboardNavigationController.setIsActive(false); this.workspaceDragger.endDrag(this.currentDragDeltaXY); } else if (this.isBubbleClick()) { // Do nothing, bubbles don't currently respond to clicks. @@ -723,24 +728,17 @@ export class Gesture { * @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); + const selection = getFocusManager().getFocusedNode(); + if (hasContextMenu(selection)) { + this.startWorkspace_?.hideChaff(!!this.flyout); + selection.showContextMenu(e); } - // TODO: Handle right-click on a bubble. e.preventDefault(); e.stopPropagation(); + keyboardNavigationController.setIsActive(false); + this.dispose(); } @@ -761,10 +759,16 @@ export class Gesture { 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); + if ( + !this.targetBlock && + !this.startBubble && + !this.startComment && + !this.startIcon + ) { + // Ensure the workspace is selected if nothing else should be. Note that + // this is focusNode() instead of focusTree() because if any active node + // is focused in the workspace it should be defocused. + getFocusManager().focusNode(ws); } this.doStart(e); @@ -871,7 +875,6 @@ export class Gesture { if (!dropdownAlreadyOpen) { this.startField.showEditor(this.mostRecentEvent); } - this.bringBlockToFront(); } /** Execute an icon click. */ @@ -881,7 +884,6 @@ export class Gesture { 'Cannot do an icon click because the start icon is undefined', ); } - this.bringBlockToFront(); this.startIcon.onClick(); } @@ -894,13 +896,16 @@ export class Gesture { 'Cannot do a block click because the target block is ' + 'undefined', ); } - if (this.targetBlock.isEnabled()) { + if (this.flyout.isBlockCreatable(this.targetBlock)) { if (!eventUtils.getGroup()) { eventUtils.setGroup(true); } const newBlock = this.flyout.createBlock(this.targetBlock); newBlock.snapToGrid(); newBlock.bumpNeighbours(); + + // If a new block was added, make sure that it's correctly focused. + getFocusManager().focusNode(newBlock); } } else { if (!this.startWorkspace_) { @@ -917,7 +922,6 @@ export class Gesture { ); eventUtils.fire(event); } - this.bringBlockToFront(); eventUtils.setGroup(false); } @@ -928,11 +932,7 @@ export class Gesture { * @param _e A pointerup event. */ private doWorkspaceClick(_e: PointerEvent) { - const ws = this.creatorWorkspace; - if (common.getSelected()) { - common.getSelected()!.unselect(); - } - this.fireWorkspaceClick(this.startWorkspace_ || ws); + this.fireWorkspaceClick(this.startWorkspace_ || this.creatorWorkspace); } /* End functions defining what actions to take to execute clicks on each type @@ -940,17 +940,6 @@ export class Gesture { // 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. */ /** @@ -1020,10 +1009,10 @@ export class Gesture { * @internal */ setStartBlock(block: BlockSvg) { - // If the gesture already went through a bubble, don't set the start block. - if (!this.startBlock && !this.startBubble) { + // If the gesture already went through a block child, don't set the start + // block. + if (!this.startBlock && !this.startBubble && !this.startIcon) { this.startBlock = block; - common.setSelected(this.startBlock); if (block.isInFlyout && block !== block.getRootBlock()) { this.setTargetBlock(block.getRootBlock()); } else { @@ -1046,6 +1035,8 @@ export class Gesture { this.setTargetBlock(block.getParent()!); } else { this.targetBlock = block; + getFocusManager().focusNode(this.targetBlock); + this.targetBlock.bringToFront(); } } diff --git a/core/grid.ts b/packages/blockly/core/grid.ts similarity index 92% rename from core/grid.ts rename to packages/blockly/core/grid.ts index e2fc054a262..2d88973adc2 100644 --- a/core/grid.ts +++ b/packages/blockly/core/grid.ts @@ -210,6 +210,9 @@ export class Grid { * @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. + * @param injectionDiv The div containing the parent workspace and all related + * workspaces and block containers. CSS variables representing SVG patterns + * will be scoped to this container. * @returns The SVG element for the grid pattern. * @internal */ @@ -217,6 +220,7 @@ export class Grid { rnd: string, gridOptions: GridOptions, defs: SVGElement, + injectionDiv?: HTMLElement, ): SVGElement { /* @@ -247,6 +251,17 @@ export class Grid { // Edge 16 doesn't handle empty patterns dom.createSvgElement(Svg.LINE, {}, gridPattern); } + + if (injectionDiv) { + // Add CSS variables scoped to the injection div referencing the created + // patterns so that CSS can apply the patterns to any element in the + // injection div. + injectionDiv.style.setProperty( + '--blocklyGridPattern', + `url(#${gridPattern.id})`, + ); + } + return gridPattern; } } diff --git a/core/icons.ts b/packages/blockly/core/icons.ts similarity index 100% rename from core/icons.ts rename to packages/blockly/core/icons.ts diff --git a/core/icons/comment_icon.ts b/packages/blockly/core/icons/comment_icon.ts similarity index 80% rename from core/icons/comment_icon.ts rename to packages/blockly/core/icons/comment_icon.ts index 24a276d877f..8f5a82c0d15 100644 --- a/core/icons/comment_icon.ts +++ b/packages/blockly/core/icons/comment_icon.ts @@ -55,10 +55,13 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable { /** The size of this comment (which is applied to the editable bubble). */ private bubbleSize = new Size(DEFAULT_BUBBLE_WIDTH, DEFAULT_BUBBLE_HEIGHT); + /** The location of the comment bubble in workspace coordinates. */ + private bubbleLocation?: Coordinate; + /** * The visibility of the bubble for this comment. * - * This is used to track what the visibile state /should/ be, not necessarily + * This is used to track what the visible 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. */ @@ -108,7 +111,7 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable { }, this.svgRoot, ); - dom.addClass(this.svgRoot!, 'blockly-icon-comment'); + dom.addClass(this.svgRoot!, 'blocklyCommentIcon'); } override dispose() { @@ -144,7 +147,13 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable { } override onLocationChange(blockOrigin: Coordinate): void { + const oldLocation = this.workspaceLocation; super.onLocationChange(blockOrigin); + if (this.bubbleLocation) { + const newLocation = this.workspaceLocation; + const delta = Coordinate.difference(newLocation, oldLocation); + this.bubbleLocation = Coordinate.sum(this.bubbleLocation, delta); + } const anchorLocation = this.getAnchorLocation(); this.textInputBubble?.setAnchorLocation(anchorLocation); } @@ -184,18 +193,42 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable { return this.bubbleSize; } + /** + * Sets the location of the comment bubble in the workspace. + */ + setBubbleLocation(location: Coordinate) { + this.bubbleLocation = location; + this.textInputBubble?.moveDuringDrag(location); + } + + /** + * @returns the location of the comment bubble in the workspace. + */ + getBubbleLocation(): Coordinate | undefined { + return this.bubbleLocation; + } + /** * @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 { + const state: CommentState = { 'text': this.text, 'pinned': this.bubbleIsVisible(), 'height': this.bubbleSize.height, 'width': this.bubbleSize.width, }; + const location = this.getBubbleLocation(); + if (location) { + state['x'] = this.sourceBlock.workspace.RTL + ? this.sourceBlock.workspace.getWidth() - + (location.x + this.bubbleSize.width) + : location.x; + state['y'] = location.y; + } + return state; } return null; } @@ -209,6 +242,16 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable { ); this.bubbleVisiblity = state['pinned'] ?? false; this.setBubbleVisible(this.bubbleVisiblity); + let x = state['x']; + const y = state['y']; + renderManagement.finishQueuedRenders().then(() => { + if (x && y) { + x = this.sourceBlock.workspace.RTL + ? this.sourceBlock.workspace.getWidth() - (x + this.bubbleSize.width) + : x; + this.setBubbleLocation(new Coordinate(x, y)); + } + }); } override onClick(): void { @@ -252,6 +295,12 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable { } } + onBubbleLocationChange(): void { + if (this.textInputBubble) { + this.bubbleLocation = this.textInputBubble.getRelativeToSurfaceXY(); + } + } + bubbleIsVisible(): boolean { return this.bubbleVisiblity; } @@ -289,6 +338,11 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable { ); } + /** See IHasBubble.getBubble. */ + getBubble(): TextInputBubble | null { + return this.textInputBubble; + } + /** * 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. @@ -310,9 +364,18 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable { this.sourceBlock.workspace as WorkspaceSvg, this.getAnchorLocation(), this.getBubbleOwnerRect(), + this, ); this.textInputBubble.setText(this.getText()); this.textInputBubble.setSize(this.bubbleSize, true); + if (this.bubbleLocation) { + this.textInputBubble.moveDuringDrag(this.bubbleLocation); + } + this.textInputBubble.addTextChangeListener(() => this.onTextChange()); + this.textInputBubble.addSizeChangeListener(() => this.onSizeChange()); + this.textInputBubble.addLocationChangeListener(() => + this.onBubbleLocationChange(), + ); } /** Hides any open bubbles owned by this comment. */ @@ -355,6 +418,12 @@ export interface CommentState { /** The width of the comment bubble. */ width?: number; + + /** The X coordinate of the comment bubble. */ + x?: number; + + /** The Y coordinate of the comment bubble. */ + y?: number; } registry.register(CommentIcon.TYPE, CommentIcon); diff --git a/core/icons/exceptions.ts b/packages/blockly/core/icons/exceptions.ts similarity index 100% rename from core/icons/exceptions.ts rename to packages/blockly/core/icons/exceptions.ts diff --git a/core/icons/icon.ts b/packages/blockly/core/icons/icon.ts similarity index 68% rename from core/icons/icon.ts rename to packages/blockly/core/icons/icon.ts index 30a6b538f6e..c8cfffaa4b6 100644 --- a/core/icons/icon.ts +++ b/packages/blockly/core/icons/icon.ts @@ -7,13 +7,18 @@ import type {Block} from '../block.js'; import type {BlockSvg} from '../block_svg.js'; import * as browserEvents from '../browser_events.js'; +import type {IContextMenu} from '../interfaces/i_contextmenu.js'; +import type {IFocusableTree} from '../interfaces/i_focusable_tree.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 * as idGenerator from '../utils/idgenerator.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 type {IconType} from './icon_types.js'; /** @@ -22,7 +27,7 @@ import type {IconType} from './icon_types.js'; * 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 { +export abstract class Icon implements IIcon, IContextMenu { /** * The position of this icon relative to its blocks top-start, * in workspace units. @@ -38,8 +43,12 @@ export abstract class Icon implements IIcon { /** The tooltip for this icon. */ protected tooltip: tooltip.TipInfo; + /** The unique ID of this icon. */ + private id: string; + constructor(protected sourceBlock: Block) { this.tooltip = sourceBlock; + this.id = idGenerator.getNextUniqueId(); } getType(): IconType { @@ -50,7 +59,10 @@ export abstract class Icon implements IIcon { if (this.svgRoot) return; // The icon has already been initialized. const svgBlock = this.sourceBlock as BlockSvg; - this.svgRoot = dom.createSvgElement(Svg.G, {'class': 'blocklyIconGroup'}); + this.svgRoot = dom.createSvgElement(Svg.G, { + 'class': 'blocklyIconGroup', + 'id': this.id, + }); svgBlock.getSvgRoot().appendChild(this.svgRoot); this.updateSvgRootOffset(); browserEvents.conditionalBind( @@ -144,4 +156,49 @@ export abstract class Icon implements IIcon { isClickableInFlyout(autoClosingFlyout: boolean): boolean { return true; } + + /** See IFocusableNode.getFocusableElement. */ + getFocusableElement(): HTMLElement | SVGElement { + const svgRoot = this.svgRoot; + if (!svgRoot) throw new Error('Attempting to focus uninitialized icon.'); + return svgRoot; + } + + /** See IFocusableNode.getFocusableTree. */ + getFocusableTree(): IFocusableTree { + return this.sourceBlock.workspace as WorkspaceSvg; + } + + /** See IFocusableNode.onNodeFocus. */ + onNodeFocus(): void { + const blockBounds = (this.sourceBlock as BlockSvg).getBoundingRectangle(); + const bounds = new Rect( + blockBounds.top + this.offsetInBlock.y, + blockBounds.top + this.offsetInBlock.y + this.getSize().height, + blockBounds.left + this.offsetInBlock.x, + blockBounds.left + this.offsetInBlock.x + this.getSize().width, + ); + (this.sourceBlock as BlockSvg).workspace.scrollBoundsIntoView(bounds); + } + + /** See IFocusableNode.onNodeBlur. */ + onNodeBlur(): void {} + + /** See IFocusableNode.canBeFocused. */ + canBeFocused(): boolean { + return true; + } + + /** + * Returns the block that this icon is attached to. + * + * @returns The block this icon is attached to. + */ + getSourceBlock(): Block { + return this.sourceBlock; + } + + showContextMenu(e: PointerEvent) { + (this.getSourceBlock() as BlockSvg).showContextMenu(e); + } } diff --git a/core/icons/icon_types.ts b/packages/blockly/core/icons/icon_types.ts similarity index 100% rename from core/icons/icon_types.ts rename to packages/blockly/core/icons/icon_types.ts diff --git a/core/icons/mutator_icon.ts b/packages/blockly/core/icons/mutator_icon.ts similarity index 98% rename from core/icons/mutator_icon.ts rename to packages/blockly/core/icons/mutator_icon.ts index eea533eab4a..9055a91ea8f 100644 --- a/core/icons/mutator_icon.ts +++ b/packages/blockly/core/icons/mutator_icon.ts @@ -118,7 +118,7 @@ export class MutatorIcon extends Icon implements IHasBubble { {'class': 'blocklyIconShape', 'r': '2.7', 'cx': '8', 'cy': '8'}, this.svgRoot, ); - dom.addClass(this.svgRoot!, 'blockly-icon-mutator'); + dom.addClass(this.svgRoot!, 'blocklyMutatorIcon'); } override dispose(): void { @@ -203,6 +203,11 @@ export class MutatorIcon extends Icon implements IHasBubble { ); } + /** See IHasBubble.getBubble. */ + getBubble(): MiniWorkspaceBubble | null { + return this.miniWorkspaceBubble; + } + /** @returns the configuration the mini workspace should have. */ private getMiniWorkspaceConfig() { const options: BlocklyOptions = { diff --git a/core/icons/registry.ts b/packages/blockly/core/icons/registry.ts similarity index 100% rename from core/icons/registry.ts rename to packages/blockly/core/icons/registry.ts diff --git a/core/icons/warning_icon.ts b/packages/blockly/core/icons/warning_icon.ts similarity index 96% rename from core/icons/warning_icon.ts rename to packages/blockly/core/icons/warning_icon.ts index b82ad10971d..f24a6a56190 100644 --- a/core/icons/warning_icon.ts +++ b/packages/blockly/core/icons/warning_icon.ts @@ -10,6 +10,7 @@ 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 {IBubble} from '../interfaces/i_bubble.js'; import type {IHasBubble} from '../interfaces/i_has_bubble.js'; import * as renderManagement from '../render_management.js'; import {Size} from '../utils.js'; @@ -90,7 +91,7 @@ export class WarningIcon extends Icon implements IHasBubble { }, this.svgRoot, ); - dom.addClass(this.svgRoot!, 'blockly-icon-warning'); + dom.addClass(this.svgRoot!, 'blocklyWarningIcon'); } override dispose() { @@ -197,6 +198,11 @@ export class WarningIcon extends Icon implements IHasBubble { ); } + /** See IHasBubble.getBubble. */ + getBubble(): IBubble | null { + return this.textBubble; + } + /** * @returns the location the bubble should be anchored to. * I.E. the middle of this icon. diff --git a/core/inject.ts b/packages/blockly/core/inject.ts similarity index 76% rename from core/inject.ts rename to packages/blockly/core/inject.ts index 40016bc23f4..1ecefa7c484 100644 --- a/core/inject.ts +++ b/packages/blockly/core/inject.ts @@ -13,13 +13,10 @@ 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'; @@ -56,8 +53,6 @@ export function inject( 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); @@ -76,17 +71,12 @@ export function inject( common.setMainWorkspace(workspace); }); - browserEvents.conditionalBind(subContainer, 'keydown', null, onKeyDown); browserEvents.conditionalBind( - dropDownDiv.getContentDiv(), + subContainer, 'keydown', null, - onKeyDown, + common.globalShortcutHandler, ); - const widgetContainer = WidgetDiv.getDiv(); - if (widgetContainer) { - browserEvents.conditionalBind(widgetContainer, 'keydown', null, onKeyDown); - } return workspace; } @@ -98,7 +88,7 @@ export function inject( * @param options Dictionary of options. * @returns Newly created SVG image. */ -function createDom(container: Element, options: Options): SVGElement { +function createDom(container: HTMLElement, 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. @@ -126,7 +116,6 @@ function createDom(container: Element, options: Options): SVGElement { 'xmlns:xlink': dom.XLINK_NS, 'version': '1.1', 'class': 'blocklySvg', - 'tabindex': '0', }, container, ); @@ -141,7 +130,12 @@ function createDom(container: Element, options: Options): SVGElement { // https://neil.fraser.name/news/2015/11/01/ const rnd = String(Math.random()).substring(2); - options.gridPattern = Grid.createDom(rnd, options.gridOptions, defs); + options.gridPattern = Grid.createDom( + rnd, + options.gridOptions, + defs, + container, + ); return svg; } @@ -153,7 +147,7 @@ function createDom(container: Element, options: Options): SVGElement { * @returns Newly created main workspace. */ function createMainWorkspace( - injectionDiv: Element, + injectionDiv: HTMLElement, svg: SVGElement, options: Options, ): WorkspaceSvg { @@ -292,32 +286,6 @@ function init(mainWorkspace: WorkspaceSvg) { } } -/** - * 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. @@ -358,68 +326,7 @@ function bindDocumentEvents() { */ 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, - ), - ); + audioMgr.load([`${pathToMedia}click.mp3`], 'click'); + audioMgr.load([`${pathToMedia}disconnect.mp3`], 'disconnect'); + audioMgr.load([`${pathToMedia}delete.mp3`], 'delete'); } diff --git a/core/inputs.ts b/packages/blockly/core/inputs.ts similarity index 100% rename from core/inputs.ts rename to packages/blockly/core/inputs.ts diff --git a/core/inputs/align.ts b/packages/blockly/core/inputs/align.ts similarity index 100% rename from core/inputs/align.ts rename to packages/blockly/core/inputs/align.ts diff --git a/core/inputs/dummy_input.ts b/packages/blockly/core/inputs/dummy_input.ts similarity index 100% rename from core/inputs/dummy_input.ts rename to packages/blockly/core/inputs/dummy_input.ts diff --git a/core/inputs/end_row_input.ts b/packages/blockly/core/inputs/end_row_input.ts similarity index 100% rename from core/inputs/end_row_input.ts rename to packages/blockly/core/inputs/end_row_input.ts diff --git a/core/inputs/input.ts b/packages/blockly/core/inputs/input.ts similarity index 95% rename from core/inputs/input.ts rename to packages/blockly/core/inputs/input.ts index 0907bf44939..90d9ba7f52b 100644 --- a/core/inputs/input.ts +++ b/packages/blockly/core/inputs/input.ts @@ -20,7 +20,7 @@ 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 {RenderedConnection} from '../rendered_connection.js'; import {Align} from './align.js'; import {inputTypes} from './input_types.js'; @@ -172,7 +172,7 @@ export class Input { // 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[] = []; + let renderList: BlockSvg[] = []; if (this.visible === visible) { return renderList; } @@ -181,15 +181,14 @@ export class Input { for (let y = 0, field; (field = this.fieldRow[y]); y++) { field.setVisible(visible); } - if (this.connection) { - const renderedConnection = this.connection as RenderedConnection; + if (this.connection && this.connection instanceof RenderedConnection) { // Has a connection. if (visible) { - renderList = renderedConnection.startTrackingAll(); + renderList = this.connection.startTrackingAll(); } else { - renderedConnection.stopTrackingAll(); + this.connection.stopTrackingAll(); } - const child = renderedConnection.targetBlock(); + const child = this.connection.targetBlock(); if (child) { child.getSvgRoot().style.display = visible ? 'block' : 'none'; } diff --git a/core/inputs/input_types.ts b/packages/blockly/core/inputs/input_types.ts similarity index 100% rename from core/inputs/input_types.ts rename to packages/blockly/core/inputs/input_types.ts diff --git a/core/inputs/statement_input.ts b/packages/blockly/core/inputs/statement_input.ts similarity index 100% rename from core/inputs/statement_input.ts rename to packages/blockly/core/inputs/statement_input.ts diff --git a/core/inputs/value_input.ts b/packages/blockly/core/inputs/value_input.ts similarity index 100% rename from core/inputs/value_input.ts rename to packages/blockly/core/inputs/value_input.ts diff --git a/core/insertion_marker_previewer.ts b/packages/blockly/core/insertion_marker_previewer.ts similarity index 92% rename from core/insertion_marker_previewer.ts rename to packages/blockly/core/insertion_marker_previewer.ts index 2343b9adc76..8b5b82468c5 100644 --- a/core/insertion_marker_previewer.ts +++ b/packages/blockly/core/insertion_marker_previewer.ts @@ -150,8 +150,17 @@ export class InsertionMarkerPreviewer implements IConnectionPreviewer { return markerConn; } - private createInsertionMarker(origBlock: BlockSvg) { - const blockJson = blocks.save(origBlock, { + /** + * Transforms the given block into a JSON representation used to construct an + * insertion marker. + * + * @param block The block to serialize and use as an insertion marker. + * @returns A JSON-formatted string corresponding to a serialized + * representation of the given block suitable for use as an insertion + * marker. + */ + protected serializeBlockToInsertionMarker(block: BlockSvg) { + const blockJson = blocks.save(block, { addCoordinates: false, addInputBlocks: false, addNextBlocks: false, @@ -160,10 +169,15 @@ export class InsertionMarkerPreviewer implements IConnectionPreviewer { if (!blockJson) { throw new Error( - `Failed to serialize source block. ${origBlock.toDevString()}`, + `Failed to serialize source block. ${block.toDevString()}`, ); } + return blockJson; + } + + private createInsertionMarker(origBlock: BlockSvg) { + const blockJson = this.serializeBlockToInsertionMarker(origBlock); const result = blocks.append(blockJson, this.workspace) as BlockSvg; // Turn shadow blocks that are created programmatically during diff --git a/core/interfaces/i_autohideable.ts b/packages/blockly/core/interfaces/i_autohideable.ts similarity index 75% rename from core/interfaces/i_autohideable.ts rename to packages/blockly/core/interfaces/i_autohideable.ts index ecdec8595a6..1193023d21b 100644 --- a/core/interfaces/i_autohideable.ts +++ b/packages/blockly/core/interfaces/i_autohideable.ts @@ -20,3 +20,8 @@ export interface IAutoHideable extends IComponent { */ autoHide(onlyClosePopups: boolean): void; } + +/** Returns true if the given object is autohideable. */ +export function isAutoHideable(obj: any): obj is IAutoHideable { + return obj && typeof obj.autoHide === 'function'; +} diff --git a/core/interfaces/i_bounded_element.ts b/packages/blockly/core/interfaces/i_bounded_element.ts similarity index 100% rename from core/interfaces/i_bounded_element.ts rename to packages/blockly/core/interfaces/i_bounded_element.ts diff --git a/core/interfaces/i_bubble.ts b/packages/blockly/core/interfaces/i_bubble.ts similarity index 92% rename from core/interfaces/i_bubble.ts rename to packages/blockly/core/interfaces/i_bubble.ts index d31ce9c9dce..553f86e9e9e 100644 --- a/core/interfaces/i_bubble.ts +++ b/packages/blockly/core/interfaces/i_bubble.ts @@ -9,11 +9,12 @@ import type {Coordinate} from '../utils/coordinate.js'; import type {IContextMenu} from './i_contextmenu.js'; import type {IDraggable} from './i_draggable.js'; +import {IFocusableNode} from './i_focusable_node.js'; /** * A bubble interface. */ -export interface IBubble extends IDraggable, IContextMenu { +export interface IBubble extends IDraggable, IContextMenu, IFocusableNode { /** * 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. diff --git a/core/interfaces/i_collapsible_toolbox_item.ts b/packages/blockly/core/interfaces/i_collapsible_toolbox_item.ts similarity index 100% rename from core/interfaces/i_collapsible_toolbox_item.ts rename to packages/blockly/core/interfaces/i_collapsible_toolbox_item.ts diff --git a/core/interfaces/i_comment_icon.ts b/packages/blockly/core/interfaces/i_comment_icon.ts similarity index 60% rename from core/interfaces/i_comment_icon.ts rename to packages/blockly/core/interfaces/i_comment_icon.ts index 9801a8d6e11..1ab5bead447 100644 --- a/core/interfaces/i_comment_icon.ts +++ b/packages/blockly/core/interfaces/i_comment_icon.ts @@ -6,6 +6,7 @@ import {CommentState} from '../icons/comment_icon.js'; import {IconType} from '../icons/icon_types.js'; +import {Coordinate} from '../utils/coordinate.js'; import {Size} from '../utils/size.js'; import {IHasBubble, hasBubble} from './i_has_bubble.js'; import {IIcon, isIcon} from './i_icon.js'; @@ -20,21 +21,27 @@ export interface ICommentIcon extends IIcon, IHasBubble, ISerializable { getBubbleSize(): Size; + setBubbleLocation(location: Coordinate): void; + + getBubbleLocation(): Coordinate | undefined; + saveState(): CommentState; loadState(state: CommentState): void; } /** Checks whether the given object is an ICommentIcon. */ -export function isCommentIcon(obj: object): obj is ICommentIcon { +export function isCommentIcon(obj: any): 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 && + typeof (obj as any).setText === 'function' && + typeof (obj as any).getText === 'function' && + typeof (obj as any).setBubbleSize === 'function' && + typeof (obj as any).getBubbleSize === 'function' && + typeof (obj as any).setBubbleLocation === 'function' && + typeof (obj as any).getBubbleLocation === 'function' && obj.getType() === IconType.COMMENT ); } diff --git a/core/interfaces/i_component.ts b/packages/blockly/core/interfaces/i_component.ts similarity index 100% rename from core/interfaces/i_component.ts rename to packages/blockly/core/interfaces/i_component.ts diff --git a/core/interfaces/i_connection_checker.ts b/packages/blockly/core/interfaces/i_connection_checker.ts similarity index 100% rename from core/interfaces/i_connection_checker.ts rename to packages/blockly/core/interfaces/i_connection_checker.ts diff --git a/core/interfaces/i_connection_previewer.ts b/packages/blockly/core/interfaces/i_connection_previewer.ts similarity index 100% rename from core/interfaces/i_connection_previewer.ts rename to packages/blockly/core/interfaces/i_connection_previewer.ts diff --git a/core/interfaces/i_contextmenu.ts b/packages/blockly/core/interfaces/i_contextmenu.ts similarity index 60% rename from core/interfaces/i_contextmenu.ts rename to packages/blockly/core/interfaces/i_contextmenu.ts index cba71259fa1..20ab49c81b3 100644 --- a/core/interfaces/i_contextmenu.ts +++ b/packages/blockly/core/interfaces/i_contextmenu.ts @@ -14,3 +14,8 @@ export interface IContextMenu { */ showContextMenu(e: Event): void; } + +/** @returns true if the given object implements IContextMenu. */ +export function hasContextMenu(obj: any): obj is IContextMenu { + return obj && typeof obj.showContextMenu === 'function'; +} diff --git a/core/interfaces/i_copyable.ts b/packages/blockly/core/interfaces/i_copyable.ts similarity index 61% rename from core/interfaces/i_copyable.ts rename to packages/blockly/core/interfaces/i_copyable.ts index b653bd20a10..8d1853967d4 100644 --- a/core/interfaces/i_copyable.ts +++ b/packages/blockly/core/interfaces/i_copyable.ts @@ -15,6 +15,14 @@ export interface ICopyable extends ISelectable { * @returns Copy metadata. */ toCopyData(): T | null; + + /** + * Whether this instance is currently copyable. The standard implementation + * is to return true if isOwnDeletable and isOwnMovable return true. + * + * @returns True if it can currently be copied. + */ + isCopyable?(): boolean; } export namespace ICopyable { @@ -25,7 +33,7 @@ export namespace ICopyable { export type ICopyData = ICopyable.ICopyData; -/** @returns true if the given object is copyable. */ +/** @returns true if the given object is an ICopyable. */ export function isCopyable(obj: any): obj is ICopyable { - return obj.toCopyData !== undefined; + return obj && typeof obj.toCopyData === 'function'; } diff --git a/core/interfaces/i_deletable.ts b/packages/blockly/core/interfaces/i_deletable.ts similarity index 83% rename from core/interfaces/i_deletable.ts rename to packages/blockly/core/interfaces/i_deletable.ts index 0467709409a..156e43ddc50 100644 --- a/core/interfaces/i_deletable.ts +++ b/packages/blockly/core/interfaces/i_deletable.ts @@ -27,8 +27,9 @@ export interface IDeletable { /** 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 + obj && + typeof obj.isDeletable === 'function' && + typeof obj.dispose === 'function' && + typeof obj.setDeleteStyle === 'function' ); } diff --git a/core/interfaces/i_delete_area.ts b/packages/blockly/core/interfaces/i_delete_area.ts similarity index 100% rename from core/interfaces/i_delete_area.ts rename to packages/blockly/core/interfaces/i_delete_area.ts diff --git a/core/interfaces/i_drag_target.ts b/packages/blockly/core/interfaces/i_drag_target.ts similarity index 100% rename from core/interfaces/i_drag_target.ts rename to packages/blockly/core/interfaces/i_drag_target.ts diff --git a/core/interfaces/i_draggable.ts b/packages/blockly/core/interfaces/i_draggable.ts similarity index 87% rename from core/interfaces/i_draggable.ts rename to packages/blockly/core/interfaces/i_draggable.ts index cb723e7b88b..9130381163f 100644 --- a/core/interfaces/i_draggable.ts +++ b/packages/blockly/core/interfaces/i_draggable.ts @@ -62,11 +62,12 @@ export interface IDragStrategy { /** 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 + obj && + typeof obj.getRelativeToSurfaceXY === 'function' && + typeof obj.isMovable === 'function' && + typeof obj.startDrag === 'function' && + typeof obj.drag === 'function' && + typeof obj.endDrag === 'function' && + typeof obj.revertDrag === 'function' ); } diff --git a/core/interfaces/i_dragger.ts b/packages/blockly/core/interfaces/i_dragger.ts similarity index 100% rename from core/interfaces/i_dragger.ts rename to packages/blockly/core/interfaces/i_dragger.ts diff --git a/core/interfaces/i_flyout.ts b/packages/blockly/core/interfaces/i_flyout.ts similarity index 96% rename from core/interfaces/i_flyout.ts rename to packages/blockly/core/interfaces/i_flyout.ts index c79be344c5a..067cd5ef20d 100644 --- a/core/interfaces/i_flyout.ts +++ b/packages/blockly/core/interfaces/i_flyout.ts @@ -7,17 +7,18 @@ // Former goog.module ID: Blockly.IFlyout import type {BlockSvg} from '../block_svg.js'; -import {FlyoutItem} from '../flyout_base.js'; +import type {FlyoutItem} from '../flyout_item.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 {IFocusableTree} from './i_focusable_tree.js'; import type {IRegistrable} from './i_registrable.js'; /** * Interface for a flyout. */ -export interface IFlyout extends IRegistrable { +export interface IFlyout extends IRegistrable, IFocusableTree { /** Whether the flyout is laid out horizontally or not. */ horizontalLayout: boolean; diff --git a/packages/blockly/core/interfaces/i_flyout_inflater.ts b/packages/blockly/core/interfaces/i_flyout_inflater.ts new file mode 100644 index 00000000000..e3c1f5db48f --- /dev/null +++ b/packages/blockly/core/interfaces/i_flyout_inflater.ts @@ -0,0 +1,51 @@ +import type {FlyoutItem} from '../flyout_item.js'; +import type {IFlyout} from './i_flyout.js'; + +export interface IFlyoutInflater { + /** + * Loads the object represented by the given state onto the workspace. + * + * Note that this method's interface is identical to that in ISerializer, to + * allow for code reuse. + * + * @param state A JSON representation of an element to inflate on the flyout. + * @param flyout The flyout on whose workspace the inflated element + * should be created. If the inflated element is an `IRenderedElement` it + * itself or the inflater should append it to the workspace; the flyout + * will not do so itself. The flyout is responsible for positioning the + * element, however. + * @returns The newly inflated flyout element. + */ + load(state: object, flyout: IFlyout): FlyoutItem; + + /** + * Returns the amount of spacing that should follow the element corresponding + * to the given JSON representation. + * + * @param state A JSON representation of the element preceding the gap. + * @param defaultGap The default gap for elements in this flyout. + * @returns The gap that should follow the given element. + */ + gapForItem(state: object, defaultGap: number): number; + + /** + * Disposes of the given element. + * + * If the element in question resides on the flyout workspace, it should remove + * itself. Implementers are not otherwise required to fully dispose of the + * element; it may be e.g. cached for performance purposes. + * + * @param element The flyout element to dispose of. + */ + disposeItem(item: FlyoutItem): void; + + /** + * Returns the type of items that this inflater is responsible for inflating. + * This should be the same as the name under which this inflater registers + * itself, as well as the value returned by `getType()` on the `FlyoutItem` + * objects returned by `load()`. + * + * @returns The type of items this inflater creates. + */ + getType(): string; +} diff --git a/packages/blockly/core/interfaces/i_focusable_node.ts b/packages/blockly/core/interfaces/i_focusable_node.ts new file mode 100644 index 00000000000..57ec1a126e1 --- /dev/null +++ b/packages/blockly/core/interfaces/i_focusable_node.ts @@ -0,0 +1,120 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {IFocusableTree} from './i_focusable_tree.js'; + +/** Represents anything that can have input focus. */ +export interface IFocusableNode { + /** + * Returns the DOM element that can be explicitly requested to receive focus. + * + * IMPORTANT: Please note that this element is expected to have a visual + * presence on the page as it will both be explicitly focused and have its + * style changed depending on its current focus state (i.e. blurred, actively + * focused, and passively focused). The element will have one of two styles + * attached (where no style indicates blurred/not focused): + * - blocklyActiveFocus + * - blocklyPassiveFocus + * + * The returned element must also have a valid ID specified, and this ID + * should be unique across the entire page. Failing to have a properly unique + * ID could result in trying to focus one node (such as via a mouse click) + * leading to another node with the same ID actually becoming focused by + * FocusManager. + * + * The returned element must be visible if the node is ever focused via + * FocusManager.focusNode() or FocusManager.focusTree(). It's allowed for an + * element to be hidden until onNodeFocus() is called, or become hidden with a + * call to onNodeBlur(). + * + * It's expected the actual returned element will not change for the lifetime + * of the node (that is, its properties can change but a new element should + * never be returned). Also, the returned element will have its tabindex + * overwritten throughout the lifecycle of this node and FocusManager. + * + * If a node requires the ability to be focused directly without first being + * focused via FocusManager then it must set its own tab index. + * + * @returns The HTMLElement or SVGElement which can both receive focus and be + * visually represented as actively or passively focused for this node. + */ + getFocusableElement(): HTMLElement | SVGElement; + + /** + * Returns the closest parent tree of this node (in cases where a tree has + * distinct trees underneath it), which represents the tree to which this node + * belongs. + * + * @returns The node's IFocusableTree. + */ + getFocusableTree(): IFocusableTree; + + /** + * Called when this node receives active focus. + * + * Note that it's fine for implementations to change visibility modifiers, but + * they should avoid the following: + * - Creating or removing DOM elements (including via the renderer or drawer). + * - Affecting focus via DOM focus() calls or the FocusManager. + * + * Implementations may consider scrolling themselves into view here; that is + * not handled by the focus manager. + */ + onNodeFocus(): void; + + /** + * Called when this node loses active focus. It may still have passive focus. + * + * This has the same implementation restrictions as onNodeFocus(). + */ + onNodeBlur(): void; + + /** + * Indicates whether this node allows focus. If this returns false then none + * of the other IFocusableNode methods will be called. + * + * Note that special care must be taken if implementations of this function + * dynamically change their return value value over the lifetime of the node + * as certain environment conditions could affect the focusability of this + * node's DOM element (such as whether the element has a positive or zero + * tabindex). Also, changing from a true to a false value while the node holds + * focus will not immediately change the current focus of the node nor + * FocusManager's internal state, and thus may result in some of the node's + * functions being called later on when defocused (since it was previously + * considered focusable at the time of being focused). + * + * Implementations should generally always return true here unless there are + * circumstances under which this node should be skipped for focus + * considerations. Examples may include being disabled, read-only, a purely + * visual decoration, or a node with no visual representation that must + * implement this interface (e.g. due to a parent interface extending it). + * Keep in mind accessibility best practices when determining whether a node + * should be focusable since even disabled and read-only elements are still + * often relevant to providing organizational context to users (particularly + * when using a screen reader). + * + * @returns Whether this node can be focused by FocusManager. + */ + canBeFocused(): boolean; +} + +/** + * Determines whether the provided object fulfills the contract of + * IFocusableNode. + * + * @param obj The object to test. + * @returns Whether the provided object can be used as an IFocusableNode. + */ +export function isFocusableNode(obj: any): obj is IFocusableNode { + return ( + obj && + typeof obj.getFocusableElement === 'function' && + typeof obj.getFocusableTree === 'function' && + typeof obj.onNodeFocus === 'function' && + typeof obj.onNodeBlur === 'function' && + typeof obj.canBeFocused === 'function' + ); +} diff --git a/packages/blockly/core/interfaces/i_focusable_tree.ts b/packages/blockly/core/interfaces/i_focusable_tree.ts new file mode 100644 index 00000000000..c33189fcdf0 --- /dev/null +++ b/packages/blockly/core/interfaces/i_focusable_tree.ts @@ -0,0 +1,144 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {IFocusableNode} from './i_focusable_node.js'; + +/** + * Represents a tree of focusable elements with its own active/passive focus + * context. + * + * Note that focus is handled by FocusManager, and tree implementations can have + * at most one IFocusableNode focused at one time. If the tree itself has focus, + * then the tree's focused node is considered 'active' ('passive' if another + * tree has focus). + * + * Focus is shared between one or more trees, where each tree can have exactly + * one active or passive node (and only one active node can exist on the whole + * page at any given time). The idea of passive focus is to provide context to + * users on where their focus will be restored upon navigating back to a + * previously focused tree. + * + * Note that if the tree's current focused node (passive or active) is needed, + * FocusableTreeTraverser.findFocusedNode can be used. + * + * Note that if specific nodes are needed to be retrieved for this tree, either + * use lookUpFocusableNode or FocusableTreeTraverser.findFocusableNodeFor. + */ +export interface IFocusableTree { + /** + * Returns the top-level focusable node of the tree. + * + * It's expected that the returned node will be focused in cases where + * FocusManager wants to focus a tree in a situation where it does not + * currently have a focused node. + */ + getRootFocusableNode(): IFocusableNode; + + /** + * Returns the IFocusableNode of this tree that should receive active focus + * when the tree itself has focus returned to it. + * + * There are some very important notes to consider about a tree's focus + * lifecycle when implementing a version of this method that doesn't return + * null: + * 1. A null previousNode does not guarantee first-time focus state as nodes + * can be deleted. + * 2. This method is only used when the tree itself is focused, either through + * tab navigation or via FocusManager.focusTree(). In many cases, the + * previously focused node will be directly focused instead which will + * bypass this method. + * 3. The default behavior (i.e. returning null here) involves either + * restoring the previous node (previousNode) or focusing the tree's root. + * 4. The provided node may sometimes no longer be valid, such as in the case + * an attempt is made to focus a node that has been recently removed from + * its parent tree. Implementations can check for the validity of the node + * in order to specialize the node to which focus should fall back. + * + * This method is largely intended to provide tree implementations with the + * means of specifying a better default node than their root. + * + * @param previousNode The node that previously held passive focus for this + * tree, or null if the tree hasn't yet been focused. + * @returns The IFocusableNode that should now receive focus, or null if + * default behavior should be used, instead. + */ + getRestoredFocusableNode( + previousNode: IFocusableNode | null, + ): IFocusableNode | null; + + /** + * Returns all directly nested trees under this tree. + * + * Note that the returned list of trees doesn't need to be stable, however all + * returned trees *do* need to be registered with FocusManager. Additionally, + * this must return actual nested trees as omitting a nested tree will affect + * how focus changes map to a specific node and its tree, potentially leading + * to user confusion. + */ + getNestedTrees(): Array; + + /** + * Returns the IFocusableNode corresponding to the specified element ID, or + * null if there's no exact node within this tree with that ID or if the ID + * corresponds to the root of the tree. + * + * This will never match against nested trees. + * + * @param id The ID of the node's focusable HTMLElement or SVGElement. + */ + lookUpFocusableNode(id: string): IFocusableNode | null; + + /** + * Called when a node of this tree has received active focus. + * + * Note that a null previousTree does not necessarily indicate that this is + * the first time Blockly is receiving focus. In fact, few assumptions can be + * made about previous focus state as a previous null tree simply indicates + * that Blockly did not hold active focus prior to this tree becoming focused + * (which can happen due to focus exiting the Blockly injection div, or for + * other cases like ephemeral focus). + * + * See IFocusableNode.onNodeFocus() as implementations have the same + * restrictions as with that method. + * + * @param node The node receiving active focus. + * @param previousTree The previous tree that held active focus, or null if + * none. + */ + onTreeFocus(node: IFocusableNode, previousTree: IFocusableTree | null): void; + + /** + * Called when the previously actively focused node of this tree is now + * passively focused and there is no other active node of this tree taking its + * place. + * + * This has the same implementation restrictions and considerations as + * onTreeFocus(). + * + * @param nextTree The next tree receiving active focus, or null if none (such + * as in the case that Blockly is entirely losing DOM focus). + */ + onTreeBlur(nextTree: IFocusableTree | null): void; +} + +/** + * Determines whether the provided object fulfills the contract of + * IFocusableTree. + * + * @param obj The object to test. + * @returns Whether the provided object can be used as an IFocusableTree. + */ +export function isFocusableTree(obj: any): obj is IFocusableTree { + return ( + obj && + typeof obj.getRootFocusableNode === 'function' && + typeof obj.getRestoredFocusableNode === 'function' && + typeof obj.getNestedTrees === 'function' && + typeof obj.lookUpFocusableNode === 'function' && + typeof obj.onTreeFocus === 'function' && + typeof obj.onTreeBlur === 'function' + ); +} diff --git a/packages/blockly/core/interfaces/i_has_bubble.ts b/packages/blockly/core/interfaces/i_has_bubble.ts new file mode 100644 index 00000000000..0c2e257a440 --- /dev/null +++ b/packages/blockly/core/interfaces/i_has_bubble.ts @@ -0,0 +1,37 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {IBubble} from './i_bubble'; + +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; + + /** + * Returns the current IBubble that implementations are managing, or null if + * there isn't one. + * + * Note that this cannot be expected to return null if bubbleIsVisible() + * returns false, i.e., the nullability of the returned bubble does not + * necessarily imply visibility. + * + * @returns The current IBubble maintained by implementations, or null if + * there is not one. + */ + getBubble(): IBubble | null; +} + +/** Type guard that checks whether the given object is a IHasBubble. */ +export function hasBubble(obj: any): obj is IHasBubble { + return ( + typeof obj.bubbleIsVisible === 'function' && + typeof obj.setBubbleVisible === 'function' && + typeof obj.getBubble === 'function' + ); +} diff --git a/core/interfaces/i_icon.ts b/packages/blockly/core/interfaces/i_icon.ts similarity index 76% rename from core/interfaces/i_icon.ts rename to packages/blockly/core/interfaces/i_icon.ts index a6159985f91..06f416424ef 100644 --- a/core/interfaces/i_icon.ts +++ b/packages/blockly/core/interfaces/i_icon.ts @@ -7,8 +7,9 @@ import type {IconType} from '../icons/icon_types.js'; import type {Coordinate} from '../utils/coordinate.js'; import type {Size} from '../utils/size.js'; +import {IFocusableNode, isFocusableNode} from './i_focusable_node.js'; -export interface IIcon { +export interface IIcon extends IFocusableNode { /** * @returns the IconType representing the type of the icon. This value should * also be used to register the icon via `Blockly.icons.registry.register`. @@ -97,18 +98,19 @@ export interface IIcon { /** 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 + isFocusableNode(obj) && + typeof (obj as IIcon).getType === 'function' && + typeof (obj as IIcon).initView === 'function' && + typeof (obj as IIcon).dispose === 'function' && + typeof (obj as IIcon).getWeight === 'function' && + typeof (obj as IIcon).getSize === 'function' && + typeof (obj as IIcon).applyColour === 'function' && + typeof (obj as IIcon).hideForInsertionMarker === 'function' && + typeof (obj as IIcon).updateEditable === 'function' && + typeof (obj as IIcon).updateCollapsed === 'function' && + typeof (obj as IIcon).isShownWhenCollapsed === 'function' && + typeof (obj as IIcon).setOffsetInBlock === 'function' && + typeof (obj as IIcon).onLocationChange === 'function' && + typeof (obj as IIcon).onClick === 'function' ); } diff --git a/core/interfaces/i_keyboard_accessible.ts b/packages/blockly/core/interfaces/i_keyboard_accessible.ts similarity index 100% rename from core/interfaces/i_keyboard_accessible.ts rename to packages/blockly/core/interfaces/i_keyboard_accessible.ts diff --git a/core/interfaces/i_legacy_procedure_blocks.ts b/packages/blockly/core/interfaces/i_legacy_procedure_blocks.ts similarity index 77% rename from core/interfaces/i_legacy_procedure_blocks.ts rename to packages/blockly/core/interfaces/i_legacy_procedure_blocks.ts index d74eaec220a..c723a5ed77c 100644 --- a/core/interfaces/i_legacy_procedure_blocks.ts +++ b/packages/blockly/core/interfaces/i_legacy_procedure_blocks.ts @@ -28,9 +28,9 @@ export interface LegacyProcedureDefBlock { /** @internal */ export function isLegacyProcedureDefBlock( - block: object, -): block is LegacyProcedureDefBlock { - return (block as any).getProcedureDef !== undefined; + obj: any, +): obj is LegacyProcedureDefBlock { + return obj && typeof obj.getProcedureDef === 'function'; } /** @internal */ @@ -41,10 +41,11 @@ export interface LegacyProcedureCallBlock { /** @internal */ export function isLegacyProcedureCallBlock( - block: object, -): block is LegacyProcedureCallBlock { + obj: any, +): obj is LegacyProcedureCallBlock { return ( - (block as any).getProcedureCall !== undefined && - (block as any).renameProcedure !== undefined + obj && + typeof obj.getProcedureCall === 'function' && + typeof obj.renameProcedure === 'function' ); } diff --git a/core/interfaces/i_metrics_manager.ts b/packages/blockly/core/interfaces/i_metrics_manager.ts similarity index 98% rename from core/interfaces/i_metrics_manager.ts rename to packages/blockly/core/interfaces/i_metrics_manager.ts index bb4d54da440..6fc0d080cc2 100644 --- a/core/interfaces/i_metrics_manager.ts +++ b/packages/blockly/core/interfaces/i_metrics_manager.ts @@ -63,7 +63,7 @@ export interface IMetricsManager { * 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}. + * of a simple toolbox, see {@link IMetricsManager.getFlyoutMetrics}. * * @returns The object with the width, height and position of the toolbox. */ diff --git a/core/interfaces/i_movable.ts b/packages/blockly/core/interfaces/i_movable.ts similarity index 100% rename from core/interfaces/i_movable.ts rename to packages/blockly/core/interfaces/i_movable.ts diff --git a/packages/blockly/core/interfaces/i_navigation_policy.ts b/packages/blockly/core/interfaces/i_navigation_policy.ts new file mode 100644 index 00000000000..8e1ce6c1005 --- /dev/null +++ b/packages/blockly/core/interfaces/i_navigation_policy.ts @@ -0,0 +1,69 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {IFocusableNode} from './i_focusable_node.js'; + +/** + * A set of rules that specify where keyboard navigation should proceed. + */ +export interface INavigationPolicy { + /** + * Returns the first child element of the given element, if any. + * + * @param current The element which the user is navigating into. + * @returns The current element's first child, or null if it has none. + */ + getFirstChild(current: T): IFocusableNode | null; + + /** + * Returns the parent element of the given element, if any. + * + * @param current The element which the user is navigating out of. + * @returns The parent element of the current element, or null if it has none. + */ + getParent(current: T): IFocusableNode | null; + + /** + * Returns the peer element following the given element, if any. + * + * @param current The element which the user is navigating past. + * @returns The next peer element of the current element, or null if there is + * none. + */ + getNextSibling(current: T): IFocusableNode | null; + + /** + * Returns the peer element preceding the given element, if any. + * + * @param current The element which the user is navigating past. + * @returns The previous peer element of the current element, or null if + * there is none. + */ + getPreviousSibling(current: T): IFocusableNode | null; + + /** + * Returns whether or not the given instance should be reachable via keyboard + * navigation. + * + * Implementors should generally return true, unless there are circumstances + * under which this item should be skipped while using keyboard navigation. + * Common examples might include being disabled, invalid, readonly, or purely + * a visual decoration. For example, while Fields are navigable, non-editable + * fields return false, since they cannot be interacted with when focused. + * + * @returns True if this element should be included in keyboard navigation. + */ + isNavigable(current: T): boolean; + + /** + * Returns whether or not this navigation policy corresponds to the type of + * the given object. + * + * @param current An instance to check whether this policy applies to. + * @returns True if the given object is of a type handled by this policy. + */ + isApplicable(current: any): current is T; +} diff --git a/core/interfaces/i_observable.ts b/packages/blockly/core/interfaces/i_observable.ts similarity index 76% rename from core/interfaces/i_observable.ts rename to packages/blockly/core/interfaces/i_observable.ts index 96a2a0bc4e8..8db0c237874 100644 --- a/core/interfaces/i_observable.ts +++ b/packages/blockly/core/interfaces/i_observable.ts @@ -20,5 +20,9 @@ export interface IObservable { * @internal */ export function isObservable(obj: any): obj is IObservable { - return obj.startPublishing !== undefined && obj.stopPublishing !== undefined; + return ( + obj && + typeof obj.startPublishing === 'function' && + typeof obj.stopPublishing === 'function' + ); } diff --git a/core/interfaces/i_parameter_model.ts b/packages/blockly/core/interfaces/i_parameter_model.ts similarity index 100% rename from core/interfaces/i_parameter_model.ts rename to packages/blockly/core/interfaces/i_parameter_model.ts diff --git a/core/interfaces/i_paster.ts b/packages/blockly/core/interfaces/i_paster.ts similarity index 92% rename from core/interfaces/i_paster.ts rename to packages/blockly/core/interfaces/i_paster.ts index 321ff118f70..128913a26b1 100644 --- a/core/interfaces/i_paster.ts +++ b/packages/blockly/core/interfaces/i_paster.ts @@ -21,5 +21,5 @@ export interface IPaster> { export function isPaster( obj: any, ): obj is IPaster> { - return obj.paste !== undefined; + return obj && typeof obj.paste === 'function'; } diff --git a/core/interfaces/i_positionable.ts b/packages/blockly/core/interfaces/i_positionable.ts similarity index 100% rename from core/interfaces/i_positionable.ts rename to packages/blockly/core/interfaces/i_positionable.ts diff --git a/core/interfaces/i_procedure_block.ts b/packages/blockly/core/interfaces/i_procedure_block.ts similarity index 76% rename from core/interfaces/i_procedure_block.ts rename to packages/blockly/core/interfaces/i_procedure_block.ts index f8538052749..3a6dc4847b9 100644 --- a/core/interfaces/i_procedure_block.ts +++ b/packages/blockly/core/interfaces/i_procedure_block.ts @@ -20,9 +20,10 @@ export interface IProcedureBlock { export function isProcedureBlock( block: Block | IProcedureBlock, ): block is IProcedureBlock { + block = block as IProcedureBlock; return ( - (block as IProcedureBlock).getProcedureModel !== undefined && - (block as IProcedureBlock).doProcedureUpdate !== undefined && - (block as IProcedureBlock).isProcedureDef !== undefined + typeof block.getProcedureModel === 'function' && + typeof block.doProcedureUpdate === 'function' && + typeof block.isProcedureDef === 'function' ); } diff --git a/core/interfaces/i_procedure_map.ts b/packages/blockly/core/interfaces/i_procedure_map.ts similarity index 100% rename from core/interfaces/i_procedure_map.ts rename to packages/blockly/core/interfaces/i_procedure_map.ts diff --git a/core/interfaces/i_procedure_model.ts b/packages/blockly/core/interfaces/i_procedure_model.ts similarity index 100% rename from core/interfaces/i_procedure_model.ts rename to packages/blockly/core/interfaces/i_procedure_model.ts diff --git a/core/interfaces/i_registrable.ts b/packages/blockly/core/interfaces/i_registrable.ts similarity index 100% rename from core/interfaces/i_registrable.ts rename to packages/blockly/core/interfaces/i_registrable.ts diff --git a/core/interfaces/i_rendered_element.ts b/packages/blockly/core/interfaces/i_rendered_element.ts similarity index 69% rename from core/interfaces/i_rendered_element.ts rename to packages/blockly/core/interfaces/i_rendered_element.ts index 7e6981ca6b1..2f82487e9be 100644 --- a/core/interfaces/i_rendered_element.ts +++ b/packages/blockly/core/interfaces/i_rendered_element.ts @@ -4,19 +4,16 @@ * SPDX-License-Identifier: Apache-2.0 */ -/** @internal */ export interface IRenderedElement { /** - * @returns The root SVG element of htis rendered element. + * @returns The root SVG element of this 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; + return obj && typeof obj.getSvgRoot === 'function'; } diff --git a/packages/blockly/core/interfaces/i_selectable.ts b/packages/blockly/core/interfaces/i_selectable.ts new file mode 100644 index 00000000000..5374f50cd3a --- /dev/null +++ b/packages/blockly/core/interfaces/i_selectable.ts @@ -0,0 +1,41 @@ +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.ISelectable + +import type {Workspace} from '../workspace.js'; +import {IFocusableNode, isFocusableNode} from './i_focusable_node.js'; + +/** + * The interface for an object that is selectable. + * + * Implementations are generally expected to use their implementations of + * onNodeFocus() and onNodeBlur() to call setSelected() with themselves and + * null, respectively, in order to ensure that selections are correctly updated + * and the selection change event is fired. + */ +export interface ISelectable extends IFocusableNode { + 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: any): obj is ISelectable { + return ( + isFocusableNode(obj) && + typeof (obj as ISelectable).id === 'string' && + typeof (obj as ISelectable).workspace === 'object' && + typeof (obj as ISelectable).select === 'function' && + typeof (obj as ISelectable).unselect === 'function' + ); +} diff --git a/core/interfaces/i_selectable_toolbox_item.ts b/packages/blockly/core/interfaces/i_selectable_toolbox_item.ts similarity index 100% rename from core/interfaces/i_selectable_toolbox_item.ts rename to packages/blockly/core/interfaces/i_selectable_toolbox_item.ts diff --git a/core/interfaces/i_serializable.ts b/packages/blockly/core/interfaces/i_serializable.ts similarity index 87% rename from core/interfaces/i_serializable.ts rename to packages/blockly/core/interfaces/i_serializable.ts index 380a277095d..99e597da37a 100644 --- a/core/interfaces/i_serializable.ts +++ b/packages/blockly/core/interfaces/i_serializable.ts @@ -24,5 +24,9 @@ export interface ISerializable { /** 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; + return ( + obj && + typeof obj.saveState === 'function' && + typeof obj.loadState === 'function' + ); } diff --git a/core/interfaces/i_serializer.ts b/packages/blockly/core/interfaces/i_serializer.ts similarity index 100% rename from core/interfaces/i_serializer.ts rename to packages/blockly/core/interfaces/i_serializer.ts diff --git a/core/interfaces/i_styleable.ts b/packages/blockly/core/interfaces/i_styleable.ts similarity index 100% rename from core/interfaces/i_styleable.ts rename to packages/blockly/core/interfaces/i_styleable.ts diff --git a/core/interfaces/i_toolbox.ts b/packages/blockly/core/interfaces/i_toolbox.ts similarity index 86% rename from core/interfaces/i_toolbox.ts rename to packages/blockly/core/interfaces/i_toolbox.ts index 2756099ec34..f5d9c9fd7c6 100644 --- a/core/interfaces/i_toolbox.ts +++ b/packages/blockly/core/interfaces/i_toolbox.ts @@ -9,13 +9,14 @@ import type {ToolboxInfo} from '../utils/toolbox.js'; import type {WorkspaceSvg} from '../workspace_svg.js'; import type {IFlyout} from './i_flyout.js'; +import type {IFocusableTree} from './i_focusable_tree.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 { +export interface IToolbox extends IRegistrable, IFocusableTree { /** Initializes the toolbox. */ init(): void; @@ -94,7 +95,7 @@ export interface IToolbox extends IRegistrable { setVisible(isVisible: boolean): void; /** - * Selects the toolbox item by it's position in the list of toolbox items. + * Selects the toolbox item by its position in the list of toolbox items. * * @param position The position of the item to select. */ @@ -107,6 +108,14 @@ export interface IToolbox extends IRegistrable { */ getSelectedItem(): IToolboxItem | null; + /** + * Sets the selected item. + * + * @param item The toolbox item to select, or null to remove the current + * selection. + */ + setSelectedItem(item: IToolboxItem | null): void; + /** Disposes of this toolbox. */ dispose(): void; } diff --git a/core/interfaces/i_toolbox_item.ts b/packages/blockly/core/interfaces/i_toolbox_item.ts similarity index 93% rename from core/interfaces/i_toolbox_item.ts rename to packages/blockly/core/interfaces/i_toolbox_item.ts index e3c9864f0c0..661624fd7e8 100644 --- a/core/interfaces/i_toolbox_item.ts +++ b/packages/blockly/core/interfaces/i_toolbox_item.ts @@ -6,10 +6,12 @@ // Former goog.module ID: Blockly.IToolboxItem +import type {IFocusableNode} from './i_focusable_node.js'; + /** * Interface for an item in the toolbox. */ -export interface IToolboxItem { +export interface IToolboxItem extends IFocusableNode { /** * Initializes the toolbox item. * This includes creating the DOM and updating the state of any items based diff --git a/core/interfaces/i_variable_backed_parameter_model.ts b/packages/blockly/core/interfaces/i_variable_backed_parameter_model.ts similarity index 82% rename from core/interfaces/i_variable_backed_parameter_model.ts rename to packages/blockly/core/interfaces/i_variable_backed_parameter_model.ts index b2042bfb2f5..444deb60105 100644 --- a/core/interfaces/i_variable_backed_parameter_model.ts +++ b/packages/blockly/core/interfaces/i_variable_backed_parameter_model.ts @@ -4,13 +4,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type {VariableModel} from '../variable_model.js'; import {IParameterModel} from './i_parameter_model.js'; +import type {IVariableModel, IVariableState} from './i_variable_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; + getVariableModel(): IVariableModel; } /** diff --git a/packages/blockly/core/interfaces/i_variable_map.ts b/packages/blockly/core/interfaces/i_variable_map.ts new file mode 100644 index 00000000000..22b4eda9012 --- /dev/null +++ b/packages/blockly/core/interfaces/i_variable_map.ts @@ -0,0 +1,65 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {IVariableModel, IVariableState} from './i_variable_model.js'; + +/** + * Variable maps are container objects responsible for storing and managing the + * set of variables referenced on a workspace. + * + * Any of these methods may define invariants about which names and types are + * legal, and throw if they are not met. + */ +export interface IVariableMap> { + /* Returns the variable corresponding to the given ID, or null if none. */ + getVariableById(id: string): T | null; + + /** + * Returns the variable with the given name, or null if not found. If `type` + * is provided, the variable's type must also match, or null should be + * returned. + */ + getVariable(name: string, type?: string): T | null; + + /* Returns a list of all variables managed by this variable map. */ + getAllVariables(): T[]; + + /** + * Returns a list of all of the variables of the given type managed by this + * variable map. + */ + getVariablesOfType(type: string): T[]; + + /** + * Returns a list of the set of types of the variables managed by this + * variable map. + */ + getTypes(): string[]; + + /** + * Creates a new variable with the given name. If ID is not specified, the + * variable map should create one. Returns the new variable. + */ + createVariable(name: string, type?: string, id?: string | null): T; + + /* Adds a variable to this variable map. */ + addVariable(variable: T): void; + + /** + * Changes the name of the given variable to the name provided and returns the + * renamed variable. + */ + renameVariable(variable: T, newName: string): T; + + /* Changes the type of the given variable and returns it. */ + changeVariableType(variable: T, newType: string): T; + + /* Deletes the given variable. */ + deleteVariable(variable: T): void; + + /* Removes all variables from this variable map. */ + clear(): void; +} diff --git a/packages/blockly/core/interfaces/i_variable_model.ts b/packages/blockly/core/interfaces/i_variable_model.ts new file mode 100644 index 00000000000..791b1072567 --- /dev/null +++ b/packages/blockly/core/interfaces/i_variable_model.ts @@ -0,0 +1,57 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {Workspace} from '../workspace.js'; + +/* Representation of a variable. */ +export interface IVariableModel { + /* Returns the unique ID of this variable. */ + getId(): string; + + /* Returns the user-visible name of this variable. */ + getName(): string; + + /** + * Returns the type of the variable like 'int' or 'string'. Does not need to be + * unique. This will default to '' which is a specific type. + */ + getType(): string; + + /* Sets the user-visible name of this variable. */ + setName(name: string): this; + + /* Sets the type of this variable. */ + setType(type: string): this; + + getWorkspace(): Workspace; + + /* Serializes this variable */ + save(): T; +} + +export interface IVariableModelStatic { + new ( + workspace: Workspace, + name: string, + type?: string, + id?: string, + ): IVariableModel; + + /** + * Creates a new IVariableModel corresponding to the given state on the + * specified workspace. This method must be static in your implementation. + */ + load(state: T, workspace: Workspace): IVariableModel; +} + +/** + * Represents the state of a given variable. + */ +export interface IVariableState { + name: string; + id: string; + type?: string; +} diff --git a/core/internal_constants.ts b/packages/blockly/core/internal_constants.ts similarity index 100% rename from core/internal_constants.ts rename to packages/blockly/core/internal_constants.ts diff --git a/packages/blockly/core/keyboard_nav/block_comment_navigation_policy.ts b/packages/blockly/core/keyboard_nav/block_comment_navigation_policy.ts new file mode 100644 index 00000000000..f2f1ab7e107 --- /dev/null +++ b/packages/blockly/core/keyboard_nav/block_comment_navigation_policy.ts @@ -0,0 +1,76 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {TextInputBubble} from '../bubbles/textinput_bubble.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; +import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; + +/** + * Set of rules controlling keyboard navigation from an TextInputBubble. + */ +export class BlockCommentNavigationPolicy + implements INavigationPolicy +{ + /** + * Returns the first child of the given block comment. + * + * @param current The block comment to return the first child of. + * @returns The text editor of the given block comment bubble. + */ + getFirstChild(current: TextInputBubble): IFocusableNode | null { + return current.getEditor(); + } + + /** + * Returns the parent of the given block comment. + * + * @param current The block comment to return the parent of. + * @returns The parent block of the given block comment. + */ + getParent(current: TextInputBubble): IFocusableNode | null { + return current.getOwner() ?? null; + } + + /** + * Returns the next peer node of the given block comment. + * + * @param _current The block comment to find the following element of. + * @returns Null. + */ + getNextSibling(_current: TextInputBubble): IFocusableNode | null { + return null; + } + + /** + * Returns the previous peer node of the given block comment. + * + * @param _current The block comment to find the preceding element of. + * @returns Null. + */ + getPreviousSibling(_current: TextInputBubble): IFocusableNode | null { + return null; + } + + /** + * Returns whether or not the given block comment can be navigated to. + * + * @param current The instance to check for navigability. + * @returns True if the given block comment can be focused. + */ + isNavigable(current: TextInputBubble): boolean { + return current.canBeFocused(); + } + + /** + * Returns whether the given object can be navigated from by this policy. + * + * @param current The object to check if this policy applies to. + * @returns True if the object is an TextInputBubble. + */ + isApplicable(current: any): current is TextInputBubble { + return current instanceof TextInputBubble; + } +} diff --git a/packages/blockly/core/keyboard_nav/block_navigation_policy.ts b/packages/blockly/core/keyboard_nav/block_navigation_policy.ts new file mode 100644 index 00000000000..9f56b538455 --- /dev/null +++ b/packages/blockly/core/keyboard_nav/block_navigation_policy.ts @@ -0,0 +1,213 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {BlockSvg} from '../block_svg.js'; +import {ConnectionType} from '../connection_type.js'; +import type {Field} from '../field.js'; +import type {Icon} from '../icons/icon.js'; +import type {IBoundedElement} from '../interfaces/i_bounded_element.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; +import {isFocusableNode} from '../interfaces/i_focusable_node.js'; +import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; +import type {ISelectable} from '../interfaces/i_selectable.js'; +import {RenderedConnection} from '../rendered_connection.js'; +import {WorkspaceSvg} from '../workspace_svg.js'; + +/** + * Set of rules controlling keyboard navigation from a block. + */ +export class BlockNavigationPolicy implements INavigationPolicy { + /** + * Returns the first child of the given block. + * + * @param current The block to return the first child of. + * @returns The first field or input of the given block, if any. + */ + getFirstChild(current: BlockSvg): IFocusableNode | null { + const candidates = getBlockNavigationCandidates(current, true); + return candidates[0]; + } + + /** + * Returns the parent of the given block. + * + * @param current The block to return the parent of. + * @returns The top block of the given block's stack, or the connection to + * which it is attached. + */ + getParent(current: BlockSvg): IFocusableNode | null { + if (current.previousConnection?.targetBlock()) { + const surroundParent = current.getSurroundParent(); + if (surroundParent) return surroundParent; + } else if (current.outputConnection?.targetBlock()) { + return current.outputConnection.targetBlock(); + } + + return current.workspace; + } + + /** + * Returns the next peer node of the given block. + * + * @param current The block to find the following element of. + * @returns The first node of the next input/stack if the given block is a terminal + * block, or its next connection. + */ + getNextSibling(current: BlockSvg): IFocusableNode | null { + if (current.nextConnection?.targetBlock()) { + return current.nextConnection?.targetBlock(); + } else if (current.outputConnection?.targetBlock()) { + return navigateBlock(current, 1); + } else if (current.getSurroundParent()) { + return navigateBlock(current.getTopStackBlock(), 1); + } else if (this.getParent(current) instanceof WorkspaceSvg) { + return navigateStacks(current, 1); + } + + return null; + } + + /** + * Returns the previous peer node of the given block. + * + * @param current The block to find the preceding element of. + * @returns The block's previous/output connection, or the last + * connection/block of the previous block stack if it is a root block. + */ + getPreviousSibling(current: BlockSvg): IFocusableNode | null { + if (current.previousConnection?.targetBlock()) { + return current.previousConnection?.targetBlock(); + } else if (current.outputConnection?.targetBlock()) { + return navigateBlock(current, -1); + } else if (this.getParent(current) instanceof WorkspaceSvg) { + return navigateStacks(current, -1); + } + + return null; + } + + /** + * Returns whether or not the given block can be navigated to. + * + * @param current The instance to check for navigability. + * @returns True if the given block can be focused. + */ + isNavigable(current: BlockSvg): boolean { + return current.canBeFocused(); + } + + /** + * Returns whether the given object can be navigated from by this policy. + * + * @param current The object to check if this policy applies to. + * @returns True if the object is a BlockSvg. + */ + isApplicable(current: any): current is BlockSvg { + return current instanceof BlockSvg; + } +} + +/** + * Returns a list of the navigable children of the given block. + * + * @param block The block to retrieve the navigable children of. + * @returns A list of navigable/focusable children of the given block. + */ +function getBlockNavigationCandidates( + block: BlockSvg, + forward: boolean, +): IFocusableNode[] { + const candidates: IFocusableNode[] = block.getIcons(); + + for (const input of block.inputList) { + if (!input.isVisible()) continue; + candidates.push(...input.fieldRow); + if (input.connection?.targetBlock()) { + const connectedBlock = input.connection.targetBlock() as BlockSvg; + if (input.connection.type === ConnectionType.NEXT_STATEMENT && !forward) { + const lastStackBlock = connectedBlock + .lastConnectionInStack(false) + ?.getSourceBlock(); + if (lastStackBlock) { + candidates.push(lastStackBlock); + } + } else { + candidates.push(connectedBlock); + } + } else if (input.connection?.type === ConnectionType.INPUT_VALUE) { + candidates.push(input.connection as RenderedConnection); + } + } + + return candidates; +} + +/** + * Returns the next/previous stack relative to the given element's stack. + * + * @param current The element whose stack will be navigated relative to. + * @param delta The difference in index to navigate; positive values navigate + * to the nth next stack, while negative values navigate to the nth previous + * stack. + * @returns The first element in the stack offset by `delta` relative to the + * current element's stack, or the last element in the stack offset by + * `delta` relative to the current element's stack when navigating backwards. + */ +export function navigateStacks(current: ISelectable, delta: number) { + const stacks: IFocusableNode[] = (current.workspace as WorkspaceSvg) + .getTopBoundedElements(true) + .filter((element: IBoundedElement) => isFocusableNode(element)); + const currentIndex = stacks.indexOf( + current instanceof BlockSvg ? current.getRootBlock() : current, + ); + const targetIndex = currentIndex + delta; + let result: IFocusableNode | null = null; + if (targetIndex >= 0 && targetIndex < stacks.length) { + result = stacks[targetIndex]; + } else if (targetIndex < 0) { + result = stacks[stacks.length - 1]; + } else if (targetIndex >= stacks.length) { + result = stacks[0]; + } + + // When navigating to a previous block stack, our previous sibling is the last + // block in it. + if (delta < 0 && result instanceof BlockSvg) { + return result.lastConnectionInStack(false)?.getSourceBlock() ?? result; + } + + return result; +} + +/** + * Returns the next navigable item relative to the provided block child. + * + * @param current The navigable block child item to navigate relative to. + * @param delta The difference in index to navigate; positive values navigate + * forward by n, while negative values navigate backwards by n. + * @returns The navigable block child offset by `delta` relative to `current`. + */ +export function navigateBlock( + current: Icon | Field | RenderedConnection | BlockSvg, + delta: number, +): IFocusableNode | null { + const block = + current instanceof BlockSvg + ? (current.outputConnection?.targetBlock() ?? current.getSurroundParent()) + : current.getSourceBlock(); + if (!(block instanceof BlockSvg)) return null; + + const candidates = getBlockNavigationCandidates(block, delta > 0); + const currentIndex = candidates.indexOf(current); + if (currentIndex === -1) return null; + + const targetIndex = currentIndex + delta; + if (targetIndex >= 0 && targetIndex < candidates.length) { + return candidates[targetIndex]; + } + + return null; +} diff --git a/packages/blockly/core/keyboard_nav/comment_bar_button_navigation_policy.ts b/packages/blockly/core/keyboard_nav/comment_bar_button_navigation_policy.ts new file mode 100644 index 00000000000..6654d2d8fef --- /dev/null +++ b/packages/blockly/core/keyboard_nav/comment_bar_button_navigation_policy.ts @@ -0,0 +1,88 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {CommentBarButton} from '../comments/comment_bar_button.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; +import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; + +/** + * Set of rules controlling keyboard navigation from a CommentBarButton. + */ +export class CommentBarButtonNavigationPolicy + implements INavigationPolicy +{ + /** + * Returns the first child of the given CommentBarButton. + * + * @param _current The CommentBarButton to return the first child of. + * @returns Null. + */ + getFirstChild(_current: CommentBarButton): IFocusableNode | null { + return null; + } + + /** + * Returns the parent of the given CommentBarButton. + * + * @param current The CommentBarButton to return the parent of. + * @returns The parent comment of the given CommentBarButton. + */ + getParent(current: CommentBarButton): IFocusableNode | null { + return current + .getCommentView() + .workspace.getCommentById(current.getCommentView().commentId); + } + + /** + * Returns the next peer node of the given CommentBarButton. + * + * @param current The CommentBarButton to find the following element of. + * @returns The next CommentBarButton, if any. + */ + getNextSibling(current: CommentBarButton): IFocusableNode | null { + const children = current.getCommentView().getCommentBarButtons(); + const currentIndex = children.indexOf(current); + if (currentIndex >= 0 && currentIndex + 1 < children.length) { + return children[currentIndex + 1]; + } + return null; + } + + /** + * Returns the previous peer node of the given CommentBarButton. + * + * @param current The CommentBarButton to find the preceding element of. + * @returns The CommentBarButton's previous CommentBarButton, if any. + */ + getPreviousSibling(current: CommentBarButton): IFocusableNode | null { + const children = current.getCommentView().getCommentBarButtons(); + const currentIndex = children.indexOf(current); + if (currentIndex > 0) { + return children[currentIndex - 1]; + } + return null; + } + + /** + * Returns whether or not the given CommentBarButton can be navigated to. + * + * @param current The instance to check for navigability. + * @returns True if the given CommentBarButton can be focused. + */ + isNavigable(current: CommentBarButton): boolean { + return current.canBeFocused(); + } + + /** + * Returns whether the given object can be navigated from by this policy. + * + * @param current The object to check if this policy applies to. + * @returns True if the object is an CommentBarButton. + */ + isApplicable(current: any): current is CommentBarButton { + return current instanceof CommentBarButton; + } +} diff --git a/packages/blockly/core/keyboard_nav/comment_editor_navigation_policy.ts b/packages/blockly/core/keyboard_nav/comment_editor_navigation_policy.ts new file mode 100644 index 00000000000..456df8e97c8 --- /dev/null +++ b/packages/blockly/core/keyboard_nav/comment_editor_navigation_policy.ts @@ -0,0 +1,54 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {CommentEditor} from '../comments/comment_editor.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; +import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; + +/** + * Set of rules controlling keyboard navigation from a comment editor. + * This is a no-op placeholder (other than isNavigable/isApplicable) since + * comment editors handle their own navigation when editing ends. + */ +export class CommentEditorNavigationPolicy + implements INavigationPolicy +{ + getFirstChild(_current: CommentEditor): IFocusableNode | null { + return null; + } + + getParent(_current: CommentEditor): IFocusableNode | null { + return null; + } + + getNextSibling(_current: CommentEditor): IFocusableNode | null { + return null; + } + + getPreviousSibling(_current: CommentEditor): IFocusableNode | null { + return null; + } + + /** + * Returns whether or not the given comment editor can be navigated to. + * + * @param current The instance to check for navigability. + * @returns False. + */ + isNavigable(current: CommentEditor): boolean { + return current.canBeFocused(); + } + + /** + * Returns whether the given object can be navigated from by this policy. + * + * @param current The object to check if this policy applies to. + * @returns True if the object is a CommentEditor. + */ + isApplicable(current: any): current is CommentEditor { + return current instanceof CommentEditor; + } +} diff --git a/packages/blockly/core/keyboard_nav/connection_navigation_policy.ts b/packages/blockly/core/keyboard_nav/connection_navigation_policy.ts new file mode 100644 index 00000000000..bf685d0635c --- /dev/null +++ b/packages/blockly/core/keyboard_nav/connection_navigation_policy.ts @@ -0,0 +1,155 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {BlockSvg} from '../block_svg.js'; +import {ConnectionType} from '../connection_type.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; +import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; +import {RenderedConnection} from '../rendered_connection.js'; +import {navigateBlock} from './block_navigation_policy.js'; + +/** + * Set of rules controlling keyboard navigation from a connection. + */ +export class ConnectionNavigationPolicy + implements INavigationPolicy +{ + /** + * Returns the first child of the given connection. + * + * @param current The connection to return the first child of. + * @returns The connection's first child element, or null if not none. + */ + getFirstChild(current: RenderedConnection): IFocusableNode | null { + if (current.getParentInput()) { + return current.targetConnection; + } + + return null; + } + + /** + * Returns the parent of the given connection. + * + * @param current The connection to return the parent of. + * @returns The given connection's parent connection or block. + */ + getParent(current: RenderedConnection): IFocusableNode | null { + return current.getSourceBlock(); + } + + /** + * Returns the next element following the given connection. + * + * @param current The connection to navigate from. + * @returns The field, input connection or block following this connection. + */ + getNextSibling(current: RenderedConnection): IFocusableNode | null { + if (current.getParentInput()) { + return navigateBlock(current, 1); + } else if (current.type === ConnectionType.NEXT_STATEMENT) { + const nextBlock = current.targetConnection; + // If this connection is the last one in the stack, our next sibling is + // the next block stack. + const sourceBlock = current.getSourceBlock(); + if ( + !nextBlock && + sourceBlock.getRootBlock().lastConnectionInStack(false) === current + ) { + const topBlocks = sourceBlock.workspace.getTopBlocks(true); + let targetIndex = topBlocks.indexOf(sourceBlock.getRootBlock()) + 1; + if (targetIndex >= topBlocks.length) { + targetIndex = 0; + } + const nextBlock = topBlocks[targetIndex]; + return this.getParentConnection(nextBlock) ?? nextBlock; + } + + return nextBlock; + } + + return current.getSourceBlock(); + } + + /** + * Returns the element preceding the given connection. + * + * @param current The connection to navigate from. + * @returns The field, input connection or block preceding this connection. + */ + getPreviousSibling(current: RenderedConnection): IFocusableNode | null { + if (current.getParentInput()) { + return navigateBlock(current, -1); + } else if ( + current.type === ConnectionType.PREVIOUS_STATEMENT || + current.type === ConnectionType.OUTPUT_VALUE + ) { + const previousConnection = + current.targetConnection && !current.targetConnection.getParentInput() + ? current.targetConnection + : null; + + // If this connection is a disconnected previous/output connection, our + // previous sibling is the previous block stack's last connection/block. + const sourceBlock = current.getSourceBlock(); + if ( + !previousConnection && + this.getParentConnection(sourceBlock.getRootBlock()) === current + ) { + const topBlocks = sourceBlock.workspace.getTopBlocks(true); + let targetIndex = topBlocks.indexOf(sourceBlock.getRootBlock()) - 1; + if (targetIndex < 0) { + targetIndex = topBlocks.length - 1; + } + const previousRootBlock = topBlocks[targetIndex]; + return ( + previousRootBlock.lastConnectionInStack(false) ?? previousRootBlock + ); + } + + return previousConnection; + } else if (current.type === ConnectionType.NEXT_STATEMENT) { + return current.getSourceBlock(); + } + return null; + } + + /** + * 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. + */ + protected getParentConnection(block: BlockSvg) { + if (!block.outputConnection || block.previousConnection?.isConnected()) { + return block.previousConnection; + } + return block.outputConnection; + } + + /** + * Returns whether or not the given connection can be navigated to. + * + * @param current The instance to check for navigability. + * @returns True if the given connection can be focused. + */ + isNavigable(current: RenderedConnection): boolean { + return current.canBeFocused(); + } + + /** + * Returns whether the given object can be navigated from by this policy. + * + * @param current The object to check if this policy applies to. + * @returns True if the object is a RenderedConnection. + */ + isApplicable(current: any): current is RenderedConnection { + return current instanceof RenderedConnection; + } +} diff --git a/packages/blockly/core/keyboard_nav/field_navigation_policy.ts b/packages/blockly/core/keyboard_nav/field_navigation_policy.ts new file mode 100644 index 00000000000..f9df406c22c --- /dev/null +++ b/packages/blockly/core/keyboard_nav/field_navigation_policy.ts @@ -0,0 +1,85 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {BlockSvg} from '../block_svg.js'; +import {Field} from '../field.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; +import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; +import {navigateBlock} from './block_navigation_policy.js'; + +/** + * Set of rules controlling keyboard navigation from a field. + */ +export class FieldNavigationPolicy implements INavigationPolicy> { + /** + * Returns null since fields do not have children. + * + * @param _current The field to navigate from. + * @returns Null. + */ + getFirstChild(_current: Field): IFocusableNode | null { + return null; + } + + /** + * Returns the parent block of the given field. + * + * @param current The field to navigate from. + * @returns The given field's parent block. + */ + getParent(current: Field): IFocusableNode | null { + return current.getSourceBlock() as BlockSvg; + } + + /** + * Returns the next field or input following the given field. + * + * @param current The field to navigate from. + * @returns The next field or input in the given field's block. + */ + getNextSibling(current: Field): IFocusableNode | null { + return navigateBlock(current, 1); + } + + /** + * Returns the field or input preceding the given field. + * + * @param current The field to navigate from. + * @returns The preceding field or input in the given field's block. + */ + getPreviousSibling(current: Field): IFocusableNode | null { + return navigateBlock(current, -1); + } + + /** + * Returns whether or not the given field can be navigated to. + * + * @param current The instance to check for navigability. + * @returns True if the given field can be focused and navigated to. + */ + isNavigable(current: Field): boolean { + return ( + current.canBeFocused() && + current.isVisible() && + (current.isClickable() || current.isCurrentlyEditable()) && + !( + current.getSourceBlock()?.isSimpleReporter() && + current.isFullBlockField() + ) && + current.getParentInput().isVisible() + ); + } + + /** + * Returns whether the given object can be navigated from by this policy. + * + * @param current The object to check if this policy applies to. + * @returns True if the object is a Field. + */ + isApplicable(current: any): current is Field { + return current instanceof Field; + } +} diff --git a/packages/blockly/core/keyboard_nav/flyout_button_navigation_policy.ts b/packages/blockly/core/keyboard_nav/flyout_button_navigation_policy.ts new file mode 100644 index 00000000000..6c39c3061e7 --- /dev/null +++ b/packages/blockly/core/keyboard_nav/flyout_button_navigation_policy.ts @@ -0,0 +1,76 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {FlyoutButton} from '../flyout_button.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; +import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; + +/** + * Set of rules controlling keyboard navigation from a flyout button. + */ +export class FlyoutButtonNavigationPolicy + implements INavigationPolicy +{ + /** + * Returns null since flyout buttons have no children. + * + * @param _current The FlyoutButton instance to navigate from. + * @returns Null. + */ + getFirstChild(_current: FlyoutButton): IFocusableNode | null { + return null; + } + + /** + * Returns the parent workspace of the given flyout button. + * + * @param current The FlyoutButton instance to navigate from. + * @returns The given flyout button's parent workspace. + */ + getParent(current: FlyoutButton): IFocusableNode | null { + return current.getWorkspace(); + } + + /** + * Returns null since inter-item navigation is done by FlyoutNavigationPolicy. + * + * @param _current The FlyoutButton instance to navigate from. + * @returns Null. + */ + getNextSibling(_current: FlyoutButton): IFocusableNode | null { + return null; + } + + /** + * Returns null since inter-item navigation is done by FlyoutNavigationPolicy. + * + * @param _current The FlyoutButton instance to navigate from. + * @returns Null. + */ + getPreviousSibling(_current: FlyoutButton): IFocusableNode | null { + return null; + } + + /** + * Returns whether or not the given flyout button can be navigated to. + * + * @param current The instance to check for navigability. + * @returns True if the given flyout button can be focused. + */ + isNavigable(current: FlyoutButton): boolean { + return current.canBeFocused(); + } + + /** + * Returns whether the given object can be navigated from by this policy. + * + * @param current The object to check if this policy applies to. + * @returns True if the object is a FlyoutButton. + */ + isApplicable(current: any): current is FlyoutButton { + return current instanceof FlyoutButton; + } +} diff --git a/packages/blockly/core/keyboard_nav/flyout_navigation_policy.ts b/packages/blockly/core/keyboard_nav/flyout_navigation_policy.ts new file mode 100644 index 00000000000..6552c27b499 --- /dev/null +++ b/packages/blockly/core/keyboard_nav/flyout_navigation_policy.ts @@ -0,0 +1,111 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {IFlyout} from '../interfaces/i_flyout.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; +import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; + +/** + * Generic navigation policy that navigates between items in the flyout. + */ +export class FlyoutNavigationPolicy implements INavigationPolicy { + /** + * Creates a new FlyoutNavigationPolicy instance. + * + * @param policy The policy to defer to for parents/children. + * @param flyout The flyout this policy will control navigation in. + */ + constructor( + private policy: INavigationPolicy, + private flyout: IFlyout, + ) {} + + /** + * Returns null to prevent navigating into flyout items. + * + * @param _current The flyout item to navigate from. + * @returns Null to prevent navigating into flyout items. + */ + getFirstChild(_current: T): IFocusableNode | null { + return null; + } + + /** + * Returns the parent of the given flyout item. + * + * @param current The flyout item to navigate from. + * @returns The parent of the given flyout item. + */ + getParent(current: T): IFocusableNode | null { + return this.policy.getParent(current); + } + + /** + * Returns the next item in the flyout relative to the given item. + * + * @param current The flyout item to navigate from. + * @returns The flyout item following the given one. + */ + getNextSibling(current: T): IFocusableNode | null { + const flyoutContents = this.flyout.getContents(); + if (!flyoutContents) return null; + + let index = flyoutContents.findIndex( + (flyoutItem) => flyoutItem.getElement() === current, + ); + + if (index === -1) return null; + index++; + if (index >= flyoutContents.length) { + index = 0; + } + + return flyoutContents[index].getElement(); + } + + /** + * Returns the previous item in the flyout relative to the given item. + * + * @param current The flyout item to navigate from. + * @returns The flyout item preceding the given one. + */ + getPreviousSibling(current: T): IFocusableNode | null { + const flyoutContents = this.flyout.getContents(); + if (!flyoutContents) return null; + + let index = flyoutContents.findIndex( + (flyoutItem) => flyoutItem.getElement() === current, + ); + + if (index === -1) return null; + index--; + if (index < 0) { + index = flyoutContents.length - 1; + } + + return flyoutContents[index].getElement(); + } + + /** + * Returns whether or not the given flyout item can be navigated to. + * + * @param current The instance to check for navigability. + * @returns True if the given flyout item can be focused. + */ + isNavigable(current: T): boolean { + return this.policy.isNavigable(current); + } + + /** + * Returns whether the given object can be navigated from by this policy. + * + * @param current The object to check if this policy applies to. + * @returns True if the object is a BlockSvg. + */ + isApplicable(current: any): current is T { + return this.policy.isApplicable(current); + } +} diff --git a/packages/blockly/core/keyboard_nav/flyout_separator_navigation_policy.ts b/packages/blockly/core/keyboard_nav/flyout_separator_navigation_policy.ts new file mode 100644 index 00000000000..eb7ca4eb783 --- /dev/null +++ b/packages/blockly/core/keyboard_nav/flyout_separator_navigation_policy.ts @@ -0,0 +1,53 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {FlyoutSeparator} from '../flyout_separator.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; +import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; + +/** + * Set of rules controlling keyboard navigation from a flyout separator. + * This is a no-op placeholder, since flyout separators can't be navigated to. + */ +export class FlyoutSeparatorNavigationPolicy + implements INavigationPolicy +{ + getFirstChild(_current: FlyoutSeparator): IFocusableNode | null { + return null; + } + + getParent(_current: FlyoutSeparator): IFocusableNode | null { + return null; + } + + getNextSibling(_current: FlyoutSeparator): IFocusableNode | null { + return null; + } + + getPreviousSibling(_current: FlyoutSeparator): IFocusableNode | null { + return null; + } + + /** + * Returns whether or not the given flyout separator can be navigated to. + * + * @param _current The instance to check for navigability. + * @returns False. + */ + isNavigable(_current: FlyoutSeparator): boolean { + return false; + } + + /** + * Returns whether the given object can be navigated from by this policy. + * + * @param current The object to check if this policy applies to. + * @returns True if the object is a FlyoutSeparator. + */ + isApplicable(current: any): current is FlyoutSeparator { + return current instanceof FlyoutSeparator; + } +} diff --git a/packages/blockly/core/keyboard_nav/icon_navigation_policy.ts b/packages/blockly/core/keyboard_nav/icon_navigation_policy.ts new file mode 100644 index 00000000000..112239d0655 --- /dev/null +++ b/packages/blockly/core/keyboard_nav/icon_navigation_policy.ts @@ -0,0 +1,93 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {BlockSvg} from '../block_svg.js'; +import {getFocusManager} from '../focus_manager.js'; +import {CommentIcon} from '../icons/comment_icon.js'; +import {Icon} from '../icons/icon.js'; +import {MutatorIcon} from '../icons/mutator_icon.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; +import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; +import {navigateBlock} from './block_navigation_policy.js'; + +/** + * Set of rules controlling keyboard navigation from an icon. + */ +export class IconNavigationPolicy implements INavigationPolicy { + /** + * Returns the first child of the given icon. + * + * @param current The icon to return the first child of. + * @returns Null. + */ + getFirstChild(current: Icon): IFocusableNode | null { + if ( + current instanceof MutatorIcon && + current.bubbleIsVisible() && + getFocusManager().getFocusedNode() === current + ) { + return current.getBubble()?.getWorkspace() ?? null; + } else if ( + current instanceof CommentIcon && + current.bubbleIsVisible() && + getFocusManager().getFocusedNode() === current + ) { + return current.getBubble()?.getEditor() ?? null; + } + + return null; + } + + /** + * Returns the parent of the given icon. + * + * @param current The icon to return the parent of. + * @returns The source block of the given icon. + */ + getParent(current: Icon): IFocusableNode | null { + return current.getSourceBlock() as BlockSvg; + } + + /** + * Returns the next peer node of the given icon. + * + * @param current The icon to find the following element of. + * @returns The next icon, field or input following this icon, if any. + */ + getNextSibling(current: Icon): IFocusableNode | null { + return navigateBlock(current, 1); + } + + /** + * Returns the previous peer node of the given icon. + * + * @param current The icon to find the preceding element of. + * @returns The icon's previous icon, if any. + */ + getPreviousSibling(current: Icon): IFocusableNode | null { + return navigateBlock(current, -1); + } + + /** + * Returns whether or not the given icon can be navigated to. + * + * @param current The instance to check for navigability. + * @returns True if the given icon can be focused. + */ + isNavigable(current: Icon): boolean { + return current.canBeFocused(); + } + + /** + * Returns whether the given object can be navigated from by this policy. + * + * @param current The object to check if this policy applies to. + * @returns True if the object is an Icon. + */ + isApplicable(current: any): current is Icon { + return current instanceof Icon; + } +} diff --git a/packages/blockly/core/keyboard_nav/line_cursor.ts b/packages/blockly/core/keyboard_nav/line_cursor.ts new file mode 100644 index 00000000000..30770e47d2d --- /dev/null +++ b/packages/blockly/core/keyboard_nav/line_cursor.ts @@ -0,0 +1,414 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview The class representing a line cursor. + * A line cursor tries to traverse the blocks and connections on a block as if + * they were lines of code in a text editor. Previous and next traverse previous + * connections, next connections and blocks, while in and out traverse input + * connections and fields. + * @author aschmiedt@google.com (Abby Schmiedt) + */ + +import {BlockSvg} from '../block_svg.js'; +import {RenderedWorkspaceComment} from '../comments/rendered_workspace_comment.js'; +import {getFocusManager} from '../focus_manager.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; +import * as registry from '../registry.js'; +import type {WorkspaceSvg} from '../workspace_svg.js'; +import {Marker} from './marker.js'; + +/** + * Class for a line cursor. + */ +export class LineCursor extends Marker { + override type = 'cursor'; + + /** Locations to try moving the cursor to after a deletion. */ + private potentialNodes: IFocusableNode[] | null = null; + + /** + * @param workspace The workspace this cursor belongs to. + */ + constructor(protected readonly workspace: WorkspaceSvg) { + super(); + } + + /** + * Moves the cursor to the next block or workspace comment in the pre-order + * traversal. + * + * @returns The next node, or null if the current node is not set or there is + * no next value. + */ + next(): IFocusableNode | null { + const curNode = this.getCurNode(); + if (!curNode) { + return null; + } + const newNode = this.getNextNode( + curNode, + (candidate: IFocusableNode | null) => { + return ( + (candidate instanceof BlockSvg && + !candidate.outputConnection?.targetBlock()) || + candidate instanceof RenderedWorkspaceComment + ); + }, + true, + ); + + if (newNode) { + this.setCurNode(newNode); + } + return newNode; + } + + /** + * Moves the cursor to the next input connection or field + * in the pre order traversal. + * + * @returns The next node, or null if the current node is + * not set or there is no next value. + */ + in(): IFocusableNode | null { + const curNode = this.getCurNode(); + if (!curNode) { + return null; + } + + const newNode = this.getNextNode(curNode, () => true, true); + + if (newNode) { + this.setCurNode(newNode); + } + return newNode; + } + /** + * Moves the cursor to the previous block or workspace comment in the + * pre-order traversal. + * + * @returns The previous node, or null if the current node is not set or there + * is no previous value. + */ + prev(): IFocusableNode | null { + const curNode = this.getCurNode(); + if (!curNode) { + return null; + } + const newNode = this.getPreviousNode( + curNode, + (candidate: IFocusableNode | null) => { + return ( + (candidate instanceof BlockSvg && + !candidate.outputConnection?.targetBlock()) || + candidate instanceof RenderedWorkspaceComment + ); + }, + true, + ); + + if (newNode) { + this.setCurNode(newNode); + } + return newNode; + } + + /** + * Moves the cursor to the previous input connection or field in the pre order + * traversal. + * + * @returns The previous node, or null if the current node + * is not set or there is no previous value. + */ + out(): IFocusableNode | null { + const curNode = this.getCurNode(); + if (!curNode) { + return null; + } + + const newNode = this.getPreviousNode(curNode, () => true, true); + + if (newNode) { + this.setCurNode(newNode); + } + return newNode; + } + + /** + * Returns true iff the node to which we would navigate if in() were + * called is the same as the node to which we would navigate if next() were + * called - in effect, if the LineCursor is at the end of the 'current + * line' of the program. + */ + atEndOfLine(): boolean { + const curNode = this.getCurNode(); + if (!curNode) return false; + const inNode = this.getNextNode(curNode, () => true, true); + const nextNode = this.getNextNode( + curNode, + (candidate: IFocusableNode | null) => { + return ( + candidate instanceof BlockSvg && + !candidate.outputConnection?.targetBlock() + ); + }, + true, + ); + + return inNode === nextNode; + } + + /** + * 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. + * @param visitedNodes A set of previously visited nodes used to avoid cycles. + * @returns The next node in the traversal. + */ + private getNextNodeImpl( + node: IFocusableNode | null, + isValid: (p1: IFocusableNode | null) => boolean, + visitedNodes: Set = new Set(), + ): IFocusableNode | null { + if (!node || visitedNodes.has(node)) return null; + let newNode = + this.workspace.getNavigator().getFirstChild(node) || + this.workspace.getNavigator().getNextSibling(node); + + let target = node; + while (target && !newNode) { + const parent = this.workspace.getNavigator().getParent(target); + if (!parent) break; + newNode = this.workspace.getNavigator().getNextSibling(parent); + target = parent; + } + + if (isValid(newNode)) return newNode; + if (newNode) { + visitedNodes.add(node); + return this.getNextNodeImpl(newNode, isValid, visitedNodes); + } + return null; + } + + /** + * Get the next node in the AST, optionally allowing for loopback. + * + * @param node The current position in the AST. + * @param isValid A function true/false depending on whether the given node + * should be traversed. + * @param loop Whether to loop around to the beginning of the workspace if no + * valid node was found. + * @returns The next node in the traversal. + */ + getNextNode( + node: IFocusableNode | null, + isValid: (p1: IFocusableNode | null) => boolean, + loop: boolean, + ): IFocusableNode | null { + if (!node || (!loop && this.getLastNode() === node)) return null; + + return this.getNextNodeImpl(node, isValid); + } + + /** + * 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. + * @param visitedNodes A set of previously visited nodes used to avoid cycles. + * @returns The previous node in the traversal or null if no previous node + * exists. + */ + private getPreviousNodeImpl( + node: IFocusableNode | null, + isValid: (p1: IFocusableNode | null) => boolean, + visitedNodes: Set = new Set(), + ): IFocusableNode | null { + if (!node || visitedNodes.has(node)) return null; + + const newNode = + this.getRightMostChild( + this.workspace.getNavigator().getPreviousSibling(node), + node, + ) || this.workspace.getNavigator().getParent(node); + + if (isValid(newNode)) return newNode; + if (newNode) { + visitedNodes.add(node); + return this.getPreviousNodeImpl(newNode, isValid, visitedNodes); + } + return null; + } + + /** + * Get the previous node in the AST, optionally allowing for loopback. + * + * @param node The current position in the AST. + * @param isValid A function true/false depending on whether the given node + * should be traversed. + * @param loop Whether to loop around to the end of the workspace if no valid + * node was found. + * @returns The previous node in the traversal or null if no previous node + * exists. + */ + getPreviousNode( + node: IFocusableNode | null, + isValid: (p1: IFocusableNode | null) => boolean, + loop: boolean, + ): IFocusableNode | null { + if (!node || (!loop && this.getFirstNode() === node)) return null; + + return this.getPreviousNodeImpl(node, isValid); + } + + /** + * 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: IFocusableNode | null, + stopIfFound: IFocusableNode, + ): IFocusableNode | null { + if (!node) return node; + let newNode = this.workspace.getNavigator().getFirstChild(node); + if (!newNode || newNode === stopIfFound) return node; + for ( + let nextNode: IFocusableNode | null = newNode; + nextNode; + nextNode = this.workspace.getNavigator().getNextSibling(newNode) + ) { + if (nextNode === stopIfFound) break; + newNode = nextNode; + } + return this.getRightMostChild(newNode, stopIfFound); + } + + /** + * Prepare for the deletion of a block by making a list of nodes we + * could move the cursor to afterwards and save it to + * this.potentialNodes. + * + * After the deletion has occurred, call postDelete to move it to + * the first valid node on that list. + * + * The locations to try (in order of preference) are: + * + * - The current location. + * - The connection to which the deleted block is attached. + * - The block connected to the next connection of the deleted block. + * - The parent block of the deleted block. + * - A location on the workspace beneath the deleted block. + * + * N.B.: When block is deleted, all of the blocks conneccted to that + * block's inputs are also deleted, but not blocks connected to its + * next connection. + * + * @param deletedBlock The block that is being deleted. + */ + preDelete(deletedBlock: BlockSvg) { + const curNode = this.getCurNode(); + + const nodes: IFocusableNode[] = curNode ? [curNode] : []; + // The connection to which the deleted block is attached. + const parentConnection = + deletedBlock.previousConnection?.targetConnection ?? + deletedBlock.outputConnection?.targetConnection; + if (parentConnection) { + nodes.push(parentConnection); + } + // The block connected to the next connection of the deleted block. + const nextBlock = deletedBlock.getNextBlock(); + if (nextBlock) { + nodes.push(nextBlock); + } + // The parent block of the deleted block. + const parentBlock = deletedBlock.getParent(); + if (parentBlock) { + nodes.push(parentBlock); + } + // A location on the workspace beneath the deleted block. + // Move to the workspace. + nodes.push(this.workspace); + this.potentialNodes = nodes; + } + + /** + * Move the cursor to the first valid location in + * this.potentialNodes, following a block deletion. + */ + postDelete() { + const nodes = this.potentialNodes; + this.potentialNodes = null; + if (!nodes) throw new Error('must call preDelete first'); + for (const node of nodes) { + if (!this.getSourceBlockFromNode(node)?.disposed) { + this.setCurNode(node); + return; + } + } + throw new Error('no valid nodes in this.potentialNodes'); + } + + /** + * Get the current location of the cursor. + * + * Overrides normal Marker getCurNode to update the current node from the + * selected block. This typically happens via the selection listener but that + * is not called immediately when `Gesture` calls + * `Blockly.common.setSelected`. In particular the listener runs after showing + * the context menu. + * + * @returns The current field, connection, or block the cursor is on. + */ + getCurNode(): IFocusableNode | null { + return getFocusManager().getFocusedNode(); + } + + /** + * Set the location of the cursor and draw it. + * + * Overrides normal Marker setCurNode logic to call + * this.drawMarker() instead of this.drawer.draw() directly. + * + * @param newNode The new location of the cursor. + */ + setCurNode(newNode: IFocusableNode) { + getFocusManager().focusNode(newNode); + } + + /** + * Get the first navigable node on the workspace, or null if none exist. + * + * @returns The first navigable node on the workspace, or null. + */ + getFirstNode(): IFocusableNode | null { + return this.workspace.getNavigator().getFirstChild(this.workspace); + } + + /** + * Get the last navigable node on the workspace, or null if none exist. + * + * @returns The last navigable node on the workspace, or null. + */ + getLastNode(): IFocusableNode | null { + const first = this.getFirstNode(); + return this.getPreviousNode(first, () => true, true); + } +} + +registry.register(registry.Type.CURSOR, registry.DEFAULT, LineCursor); diff --git a/packages/blockly/core/keyboard_nav/marker.ts b/packages/blockly/core/keyboard_nav/marker.ts new file mode 100644 index 00000000000..0cd066c163c --- /dev/null +++ b/packages/blockly/core/keyboard_nav/marker.ts @@ -0,0 +1,86 @@ +/** + * @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 {BlockSvg} from '../block_svg.js'; +import {Field} from '../field.js'; +import {Icon} from '../icons/icon.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; +import {RenderedConnection} from '../rendered_connection.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. */ + protected curNode: IFocusableNode | null = null; + + /** The type of the marker. */ + type = 'marker'; + + /** + * Gets the current location of the marker. + * + * @returns The current field, connection, or block the marker is on. + */ + getCurNode(): IFocusableNode | null { + return this.curNode; + } + + /** + * Set the location of the marker and call the update method. + * + * @param newNode The new location of the marker, or null to remove it. + */ + setCurNode(newNode: IFocusableNode | null) { + this.curNode = newNode; + } + + /** Dispose of this marker. */ + dispose() { + this.curNode = null; + } + + /** + * Returns the block that the given node is a child of. + * + * @returns The parent block of the node if any, otherwise null. + */ + getSourceBlockFromNode(node: IFocusableNode | null): BlockSvg | null { + if (node instanceof BlockSvg) { + return node; + } else if (node instanceof Field) { + return node.getSourceBlock() as BlockSvg; + } else if (node instanceof RenderedConnection) { + return node.getSourceBlock(); + } else if (node instanceof Icon) { + return node.getSourceBlock() as BlockSvg; + } + + return null; + } + + /** + * Returns the block that this marker's current node is a child of. + * + * @returns The parent block of the marker's current node if any, otherwise + * null. + */ + getSourceBlock(): BlockSvg | null { + return this.getSourceBlockFromNode(this.getCurNode()); + } +} diff --git a/packages/blockly/core/keyboard_nav/workspace_comment_navigation_policy.ts b/packages/blockly/core/keyboard_nav/workspace_comment_navigation_policy.ts new file mode 100644 index 00000000000..7fe70ceadef --- /dev/null +++ b/packages/blockly/core/keyboard_nav/workspace_comment_navigation_policy.ts @@ -0,0 +1,77 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {RenderedWorkspaceComment} from '../comments/rendered_workspace_comment.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; +import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; +import {navigateStacks} from './block_navigation_policy.js'; + +/** + * Set of rules controlling keyboard navigation from an RenderedWorkspaceComment. + */ +export class WorkspaceCommentNavigationPolicy + implements INavigationPolicy +{ + /** + * Returns the first child of the given workspace comment. + * + * @param current The workspace comment to return the first child of. + * @returns The first child button of the given comment. + */ + getFirstChild(current: RenderedWorkspaceComment): IFocusableNode | null { + return current.view.getCommentBarButtons()[0]; + } + + /** + * Returns the parent of the given workspace comment. + * + * @param current The workspace comment to return the parent of. + * @returns The parent workspace of the given comment. + */ + getParent(current: RenderedWorkspaceComment): IFocusableNode | null { + return current.workspace; + } + + /** + * Returns the next peer node of the given workspace comment. + * + * @param current The workspace comment to find the following element of. + * @returns The next workspace comment or block stack, if any. + */ + getNextSibling(current: RenderedWorkspaceComment): IFocusableNode | null { + return navigateStacks(current, 1); + } + + /** + * Returns the previous peer node of the given workspace comment. + * + * @param current The workspace comment to find the preceding element of. + * @returns The previous workspace comment or block stack, if any. + */ + getPreviousSibling(current: RenderedWorkspaceComment): IFocusableNode | null { + return navigateStacks(current, -1); + } + + /** + * Returns whether or not the given workspace comment can be navigated to. + * + * @param current The instance to check for navigability. + * @returns True if the given workspace comment can be focused. + */ + isNavigable(current: RenderedWorkspaceComment): boolean { + return current.canBeFocused(); + } + + /** + * Returns whether the given object can be navigated from by this policy. + * + * @param current The object to check if this policy applies to. + * @returns True if the object is an RenderedWorkspaceComment. + */ + isApplicable(current: any): current is RenderedWorkspaceComment { + return current instanceof RenderedWorkspaceComment; + } +} diff --git a/packages/blockly/core/keyboard_nav/workspace_navigation_policy.ts b/packages/blockly/core/keyboard_nav/workspace_navigation_policy.ts new file mode 100644 index 00000000000..b671f8fe739 --- /dev/null +++ b/packages/blockly/core/keyboard_nav/workspace_navigation_policy.ts @@ -0,0 +1,77 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; +import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; +import {WorkspaceSvg} from '../workspace_svg.js'; + +/** + * Set of rules controlling keyboard navigation from a workspace. + */ +export class WorkspaceNavigationPolicy + implements INavigationPolicy +{ + /** + * Returns the first child of the given workspace. + * + * @param current The workspace to return the first child of. + * @returns The top block of the first block stack, if any. + */ + getFirstChild(current: WorkspaceSvg): IFocusableNode | null { + const blocks = current.getTopBlocks(true); + return blocks.length ? blocks[0] : null; + } + + /** + * Returns the parent of the given workspace. + * + * @param _current The workspace to return the parent of. + * @returns Null. + */ + getParent(_current: WorkspaceSvg): IFocusableNode | null { + return null; + } + + /** + * Returns the next sibling of the given workspace. + * + * @param _current The workspace to return the next sibling of. + * @returns Null. + */ + getNextSibling(_current: WorkspaceSvg): IFocusableNode | null { + return null; + } + + /** + * Returns the previous sibling of the given workspace. + * + * @param _current The workspace to return the previous sibling of. + * @returns Null. + */ + getPreviousSibling(_current: WorkspaceSvg): IFocusableNode | null { + return null; + } + + /** + * Returns whether or not the given workspace can be navigated to. + * + * @param current The instance to check for navigability. + * @returns True if the given workspace can be focused. + */ + isNavigable(current: WorkspaceSvg): boolean { + return current.canBeFocused() && !current.isMutator; + } + + /** + * Returns whether the given object can be navigated from by this policy. + * + * @param current The object to check if this policy applies to. + * @returns True if the object is a WorkspaceSvg. + */ + isApplicable(current: any): current is WorkspaceSvg { + return current instanceof WorkspaceSvg; + } +} diff --git a/packages/blockly/core/keyboard_navigation_controller.ts b/packages/blockly/core/keyboard_navigation_controller.ts new file mode 100644 index 00000000000..d0a766daff2 --- /dev/null +++ b/packages/blockly/core/keyboard_navigation_controller.ts @@ -0,0 +1,63 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * The KeyboardNavigationController handles coordinating Blockly-wide + * keyboard navigation behavior, such as enabling/disabling full + * cursor visualization. + */ +export class KeyboardNavigationController { + /** Whether the user is actively using keyboard navigation. */ + private isActive = false; + /** Css class name added to body if keyboard nav is active. */ + private activeClassName = 'blocklyKeyboardNavigation'; + + /** + * Sets whether a user is actively using keyboard navigation. + * + * If they are, apply a css class to the entire page so that + * focused items can apply additional styling for keyboard users. + * + * Note that since enabling keyboard navigation presents significant UX changes + * (such as cursor visualization and move mode), callers should take care to + * only set active keyboard navigation when they have a high confidence in that + * being the correct state. In general, in any given mouse or key input situation + * callers can choose one of three paths: + * 1. Do nothing. This should be the choice for neutral actions that don't + * predominantly imply keyboard or mouse usage (such as clicking to select a block). + * 2. Disable keyboard navigation. This is the best choice when a user is definitely + * predominantly using the mouse (such as using a right click to open the context menu). + * 3. Enable keyboard navigation. This is the best choice when there's high confidence + * a user actually intends to use it (such as attempting to use the arrow keys to move + * around). + * + * @param isUsing + */ + setIsActive(isUsing: boolean = true) { + this.isActive = isUsing; + this.updateActiveVisualization(); + } + + /** + * @returns true if the user is actively using keyboard navigation + * (e.g., has recently taken some action that is only relevant to keyboard users) + */ + getIsActive(): boolean { + return this.isActive; + } + + /** Adds or removes the css class that indicates keyboard navigation is active. */ + private updateActiveVisualization() { + if (this.isActive) { + document.body.classList.add(this.activeClassName); + } else { + document.body.classList.remove(this.activeClassName); + } + } +} + +/** Singleton instance of the keyboard navigation controller. */ +export const keyboardNavigationController = new KeyboardNavigationController(); diff --git a/packages/blockly/core/label_flyout_inflater.ts b/packages/blockly/core/label_flyout_inflater.ts new file mode 100644 index 00000000000..ffa69ae4806 --- /dev/null +++ b/packages/blockly/core/label_flyout_inflater.ts @@ -0,0 +1,75 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {FlyoutButton} from './flyout_button.js'; +import {FlyoutItem} from './flyout_item.js'; +import type {IFlyout} from './interfaces/i_flyout.js'; +import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js'; +import * as registry from './registry.js'; +import {ButtonOrLabelInfo} from './utils/toolbox.js'; +const LABEL_TYPE = 'label'; + +/** + * Class responsible for creating labels for flyouts. + */ +export class LabelFlyoutInflater implements IFlyoutInflater { + /** + * Inflates a flyout label from the given state and adds it to the flyout. + * + * @param state A JSON representation of a flyout label. + * @param flyout The flyout to create the label on. + * @returns A FlyoutButton configured as a label. + */ + load(state: object, flyout: IFlyout): FlyoutItem { + const label = new FlyoutButton( + flyout.getWorkspace(), + flyout.targetWorkspace!, + state as ButtonOrLabelInfo, + true, + ); + label.show(); + + return new FlyoutItem(label, LABEL_TYPE); + } + + /** + * Returns the amount of space that should follow this label. + * + * @param state A JSON representation of a flyout label. + * @param defaultGap The default spacing for flyout items. + * @returns The amount of space that should follow this label. + */ + gapForItem(state: object, defaultGap: number): number { + return defaultGap; + } + + /** + * Disposes of the given label. + * + * @param item The flyout label to dispose of. + */ + disposeItem(item: FlyoutItem): void { + const element = item.getElement(); + if (element instanceof FlyoutButton) { + element.dispose(); + } + } + + /** + * Returns the type of items this inflater is responsible for creating. + * + * @returns An identifier for the type of items this inflater creates. + */ + getType() { + return LABEL_TYPE; + } +} + +registry.register( + registry.Type.FLYOUT_INFLATER, + LABEL_TYPE, + LabelFlyoutInflater, +); diff --git a/core/layer_manager.ts b/packages/blockly/core/layer_manager.ts similarity index 71% rename from core/layer_manager.ts rename to packages/blockly/core/layer_manager.ts index e7663b1b7ee..7d253b11042 100644 --- a/core/layer_manager.ts +++ b/packages/blockly/core/layer_manager.ts @@ -4,6 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +import {getFocusManager} from './focus_manager.js'; +import type {IFocusableNode} from './interfaces/i_focusable_node.js'; import {IRenderedElement} from './interfaces/i_rendered_element.js'; import * as layerNums from './layers.js'; import {Coordinate} from './utils/coordinate.js'; @@ -71,11 +73,11 @@ export class LayerManager { * @internal */ appendToAnimationLayer(elem: IRenderedElement) { - const currentTransform = this.dragLayer?.getAttribute('transform'); + const currentTransform = this.dragLayer?.style.transform; // Only update the current transform when appending, so animations don't // move if the workspace moves. - if (currentTransform) { - this.animationLayer?.setAttribute('transform', currentTransform); + if (currentTransform && this.animationLayer) { + this.animationLayer.style.transform = currentTransform; } this.animationLayer?.appendChild(elem.getSvgRoot()); } @@ -86,10 +88,12 @@ export class LayerManager { * @internal */ translateLayers(newCoord: Coordinate, newScale: number) { - const translation = `translate(${newCoord.x}, ${newCoord.y}) scale(${newScale})`; - this.dragLayer?.setAttribute('transform', translation); + const translation = `translate(${newCoord.x}px, ${newCoord.y}px) scale(${newScale})`; + if (this.dragLayer) { + this.dragLayer.style.transform = translation; + } for (const [_, layer] of this.layers) { - layer.setAttribute('transform', translation); + layer.style.transform = translation; } } @@ -97,19 +101,44 @@ export class LayerManager { * Moves the given element to the drag layer, which exists on top of all other * layers, and the drag surface. * + * @param elem The element to move onto the drag layer. + * @param focus Whether or not to focus the element post-move. + * * @internal */ - moveToDragLayer(elem: IRenderedElement) { + moveToDragLayer(elem: IRenderedElement & IFocusableNode, focus = true) { this.dragLayer?.appendChild(elem.getSvgRoot()); + + if (focus && elem.canBeFocused()) { + // Since moving the element to the drag layer will cause it to lose focus, + // ensure it regains focus (to ensure proper highlights & sent events). + getFocusManager().focusNode(elem); + } } /** * Moves the given element off of the drag layer. * + * @param elem The element to move off of the drag layer. + * @param layerNum The identifier of the layer to move the element onto. + * Should be a constant from layers.ts. + * @param focus Whether or not the element should be focused once moved onto + * the destination layer. + * * @internal */ - moveOffDragLayer(elem: IRenderedElement, layerNum: number) { + moveOffDragLayer( + elem: IRenderedElement & IFocusableNode, + layerNum: number, + focus = true, + ) { this.append(elem, layerNum); + + if (focus && elem.canBeFocused()) { + // Since moving the element off the drag layer will cause it to lose focus, + // ensure it regains focus (to ensure proper highlights & sent events). + getFocusManager().focusNode(elem); + } } /** @@ -122,7 +151,12 @@ export class LayerManager { if (!this.layers.has(layerNum)) { this.createLayer(layerNum); } - this.layers.get(layerNum)?.appendChild(elem.getSvgRoot()); + const childElem = elem.getSvgRoot(); + if (this.layers.get(layerNum)?.lastChild !== childElem) { + // Only append the child if it isn't already last (to avoid re-firing + // events like focused). + this.layers.get(layerNum)?.appendChild(childElem); + } } /** @@ -183,4 +217,13 @@ export class LayerManager { getBubbleLayer(): SVGGElement { return this.layers.get(layerNums.BUBBLE)!; } + + /** + * Returns the drag layer. + * + * @internal + */ + getDragLayer(): SVGGElement | undefined { + return this.dragLayer; + } } diff --git a/core/layers.ts b/packages/blockly/core/layers.ts similarity index 100% rename from core/layers.ts rename to packages/blockly/core/layers.ts diff --git a/core/main.ts b/packages/blockly/core/main.ts similarity index 100% rename from core/main.ts rename to packages/blockly/core/main.ts diff --git a/core/marker_manager.ts b/packages/blockly/core/marker_manager.ts similarity index 53% rename from core/marker_manager.ts rename to packages/blockly/core/marker_manager.ts index d7035534da7..e94aa3e966a 100644 --- a/core/marker_manager.ts +++ b/packages/blockly/core/marker_manager.ts @@ -11,7 +11,7 @@ */ // Former goog.module ID: Blockly.MarkerManager -import type {Cursor} from './keyboard_nav/cursor.js'; +import {LineCursor} from './keyboard_nav/line_cursor.js'; import type {Marker} from './keyboard_nav/marker.js'; import type {WorkspaceSvg} from './workspace_svg.js'; @@ -23,22 +23,18 @@ export class MarkerManager { static readonly LOCAL_MARKER = 'local_marker_1'; /** The cursor. */ - private cursor: Cursor | null = null; - - /** The cursor's SVG element. */ - private cursorSvg: SVGElement | null = null; + private cursor: LineCursor; /** The map of markers for the workspace. */ private markers = new Map(); - /** The marker's SVG element. */ - private markerSvg: SVGElement | null = null; - /** * @param workspace The workspace for the marker manager. * @internal */ - constructor(private readonly workspace: WorkspaceSvg) {} + constructor(private readonly workspace: WorkspaceSvg) { + this.cursor = new LineCursor(this.workspace); + } /** * Register the marker by adding it to the map of markers. @@ -50,10 +46,6 @@ export class MarkerManager { if (this.markers.has(id)) { this.unregisterMarker(id); } - marker.setDrawer( - this.workspace.getRenderer().makeMarkerDrawer(this.workspace, marker), - ); - this.setMarkerSvg(marker.getDrawer().createDom()); this.markers.set(id, marker); } @@ -82,7 +74,7 @@ export class MarkerManager { * * @returns The cursor for this workspace. */ - getCursor(): Cursor | null { + getCursor(): LineCursor { return this.cursor; } @@ -103,70 +95,8 @@ export class MarkerManager { * * @param cursor The cursor used to move around this workspace. */ - setCursor(cursor: Cursor) { - if (this.cursor && this.cursor.getDrawer()) { - this.cursor.getDrawer().dispose(); - } + setCursor(cursor: LineCursor) { this.cursor = cursor; - if (this.cursor) { - const drawer = this.workspace - .getRenderer() - .makeMarkerDrawer(this.workspace, this.cursor); - this.cursor.setDrawer(drawer); - this.setCursorSvg(this.cursor.getDrawer().createDom()); - } - } - - /** - * Add the cursor SVG to this workspace SVG group. - * - * @param cursorSvg The SVG root of the cursor to be added to the workspace - * SVG group. - * @internal - */ - setCursorSvg(cursorSvg: SVGElement | null) { - if (!cursorSvg) { - this.cursorSvg = null; - return; - } - - this.workspace.getBlockCanvas()!.appendChild(cursorSvg); - this.cursorSvg = cursorSvg; - } - - /** - * Add the marker SVG to this workspaces SVG group. - * - * @param markerSvg The SVG root of the marker to be added to the workspace - * SVG group. - * @internal - */ - setMarkerSvg(markerSvg: SVGElement | null) { - if (!markerSvg) { - this.markerSvg = null; - return; - } - - if (this.workspace.getBlockCanvas()) { - if (this.cursorSvg) { - this.workspace - .getBlockCanvas()! - .insertBefore(markerSvg, this.cursorSvg); - } else { - this.workspace.getBlockCanvas()!.appendChild(markerSvg); - } - } - } - - /** - * Redraw the attached cursor SVG if needed. - * - * @internal - */ - updateMarkers() { - if (this.workspace.keyboardAccessibilityMode && this.cursorSvg) { - this.workspace.getCursor()!.draw(); - } } /** @@ -181,9 +111,6 @@ export class MarkerManager { this.unregisterMarker(markerId); } this.markers.clear(); - if (this.cursor) { - this.cursor.dispose(); - this.cursor = null; - } + this.cursor.dispose(); } } diff --git a/core/menu.ts b/packages/blockly/core/menu.ts similarity index 90% rename from core/menu.ts rename to packages/blockly/core/menu.ts index ee54c8cf2c3..a064489bae8 100644 --- a/core/menu.ts +++ b/packages/blockly/core/menu.ts @@ -12,10 +12,10 @@ // Former goog.module ID: Blockly.Menu import * as browserEvents from './browser_events.js'; -import type {MenuItem} from './menuitem.js'; +import type {MenuSeparator} from './menu_separator.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 type {Size} from './utils/size.js'; import * as style from './utils/style.js'; @@ -24,11 +24,9 @@ import * as style from './utils/style.js'; */ export class Menu { /** - * Array of menu items. - * (Nulls are never in the array, but typing the array as nullable prevents - * the compiler from objecting to .indexOf(null)) + * Array of menu items and separators. */ - private readonly menuItems: MenuItem[] = []; + private readonly menuItems: Array = []; /** * Coordinates of the pointerdown event that caused this menu to open. Used to @@ -70,10 +68,10 @@ export class Menu { /** * Add a new menu item to the bottom of this menu. * - * @param menuItem Menu item to append. + * @param menuItem Menu item or separator to append. * @internal */ - addChild(menuItem: MenuItem) { + addChild(menuItem: MenuItem | MenuSeparator) { this.menuItems.push(menuItem); } @@ -83,10 +81,10 @@ export class Menu { * @param container Element upon which to append this menu. * @returns The menu's root DOM element. */ + render(container: Element): HTMLDivElement { const element = document.createElement('div'); - // goog-menu is deprecated, use blocklyMenu. May 2020. - element.className = 'blocklyMenu goog-menu blocklyNonSelectable'; + element.className = 'blocklyMenu'; element.tabIndex = 0; if (this.roleName) { aria.setRole(element, this.roleName); @@ -157,7 +155,6 @@ export class Menu { const el = this.getElement(); if (el) { el.focus({preventScroll: true}); - dom.addClass(el, 'blocklyFocused'); } } @@ -166,7 +163,6 @@ export class Menu { const el = this.getElement(); if (el) { el.blur(); - dom.removeClass(el, 'blocklyFocused'); } } @@ -230,7 +226,8 @@ export class Menu { while (currentElement && currentElement !== menuElem) { if (currentElement.classList.contains('blocklyMenuItem')) { // Having found a menu item's div, locate that menu item in this menu. - for (let i = 0, menuItem; (menuItem = this.menuItems[i]); i++) { + const items = this.getMenuItems(); + for (let i = 0, menuItem; (menuItem = items[i]); i++) { if (menuItem.getElement() === currentElement) { return menuItem; } @@ -261,11 +258,10 @@ export class Menu { // Bring the highlighted item into view. This has no effect if the menu is // not scrollable. const menuElement = this.getElement(); - const scrollingParent = menuElement?.parentElement; const menuItemElement = item.getElement(); - if (!scrollingParent || !menuItemElement) return; + if (!menuElement || !menuItemElement) return; - style.scrollIntoContainerView(menuItemElement, scrollingParent); + style.scrollIntoContainerView(menuItemElement, menuElement); aria.setState(menuElement, aria.State.ACTIVEDESCENDANT, item.getId()); } } @@ -278,7 +274,7 @@ export class Menu { */ highlightNext() { const index = this.highlightedItem - ? this.menuItems.indexOf(this.highlightedItem) + ? this.getMenuItems().indexOf(this.highlightedItem) : -1; this.highlightHelper(index, 1); } @@ -291,7 +287,7 @@ export class Menu { */ highlightPrevious() { const index = this.highlightedItem - ? this.menuItems.indexOf(this.highlightedItem) + ? this.getMenuItems().indexOf(this.highlightedItem) : -1; this.highlightHelper(index < 0 ? this.menuItems.length : index, -1); } @@ -316,7 +312,8 @@ export class Menu { private highlightHelper(startIndex: number, delta: number) { let index = startIndex + delta; let menuItem; - while ((menuItem = this.menuItems[index])) { + const items = this.getMenuItems(); + while ((menuItem = items[index])) { if (menuItem.isEnabled()) { this.setHighlighted(menuItem); break; @@ -381,7 +378,7 @@ export class Menu { const menuItem = this.getMenuItem(e.target as Element); if (menuItem) { - menuItem.performAction(); + menuItem.performAction(e); } } @@ -408,9 +405,7 @@ export class Menu { // Keyboard events. /** - * Attempts to handle a keyboard event, if the menu item is enabled, by - * calling - * {@link Menu#handleKeyEventInternal_}. + * Attempts to handle a keyboard event. * * @param e Key event to handle. */ @@ -435,7 +430,7 @@ export class Menu { case 'Enter': case ' ': if (highlighted) { - highlighted.performAction(); + highlighted.performAction(e); } break; @@ -479,4 +474,13 @@ export class Menu { menuSize.height = menuDom.scrollHeight; return menuSize; } + + /** + * Returns the action menu items (omitting separators) in this menu. + * + * @returns The MenuItem objects displayed in this menu. + */ + private getMenuItems(): MenuItem[] { + return this.menuItems.filter((item) => item instanceof MenuItem); + } } diff --git a/packages/blockly/core/menu_separator.ts b/packages/blockly/core/menu_separator.ts new file mode 100644 index 00000000000..6f7f468ad62 --- /dev/null +++ b/packages/blockly/core/menu_separator.ts @@ -0,0 +1,38 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as aria from './utils/aria.js'; + +/** + * Representation of a section separator in a menu. + */ +export class MenuSeparator { + /** + * DOM element representing this separator in a menu. + */ + private element: HTMLHRElement | null = null; + + /** + * Creates the DOM representation of this separator. + * + * @returns An
element. + */ + createDom(): HTMLHRElement { + this.element = document.createElement('hr'); + this.element.className = 'blocklyMenuSeparator'; + aria.setRole(this.element, aria.Role.SEPARATOR); + + return this.element; + } + + /** + * Disposes of this separator. + */ + dispose() { + this.element?.remove(); + this.element = null; + } +} diff --git a/core/menuitem.ts b/packages/blockly/core/menuitem.ts similarity index 68% rename from core/menuitem.ts rename to packages/blockly/core/menuitem.ts index e9e7dc0dbca..454e35744ef 100644 --- a/core/menuitem.ts +++ b/packages/blockly/core/menuitem.ts @@ -12,7 +12,6 @@ // Former goog.module ID: Blockly.MenuItem import * as aria from './utils/aria.js'; -import * as dom from './utils/dom.js'; import * as idGenerator from './utils/idgenerator.js'; /** @@ -41,7 +40,8 @@ export class MenuItem { private highlight = false; /** Bound function to call when this menu item is clicked. */ - private actionHandler: ((obj: this) => void) | null = null; + private actionHandler: ((obj: this, menuSelectEvent: Event) => void) | null = + null; /** * @param content Text caption to display as the content of the item, or a @@ -64,24 +64,15 @@ export class MenuItem { this.element = element; // Set class and style - // goog-menuitem* is deprecated, use blocklyMenuItem*. May 2020. element.className = - 'blocklyMenuItem goog-menuitem ' + - (this.enabled ? '' : 'blocklyMenuItemDisabled goog-menuitem-disabled ') + - (this.checked ? 'blocklyMenuItemSelected goog-option-selected ' : '') + - (this.highlight - ? 'blocklyMenuItemHighlight goog-menuitem-highlight ' - : '') + - (this.rightToLeft ? 'blocklyMenuItemRtl goog-menuitem-rtl ' : ''); + 'blocklyMenuItem ' + + (this.enabled ? '' : 'blocklyMenuItemDisabled ') + + (this.checked ? 'blocklyMenuItemSelected ' : '') + + (this.highlight ? 'blocklyMenuItemHighlight ' : '') + + (this.rightToLeft ? 'blocklyMenuItemRtl ' : ''); const content = document.createElement('div'); - content.className = 'blocklyMenuItemContent goog-menuitem-content'; - // Add a checkbox for checkable menu items. - if (this.checkable) { - const checkbox = document.createElement('div'); - checkbox.className = 'blocklyMenuItemCheckbox goog-menuitem-checkbox'; - content.appendChild(checkbox); - } + content.className = 'blocklyMenuItemContent'; let contentDom: Node = this.content as HTMLElement; if (typeof this.content === 'string') { @@ -90,6 +81,11 @@ export class MenuItem { content.appendChild(contentDom); element.appendChild(content); + // Add a checkbox for checkable menu items. + if (this.checkable) { + this.toggleHasCheckbox(true); + } + // Initialize ARIA role and state. if (this.roleName) { aria.setRole(element, this.roleName); @@ -147,6 +143,7 @@ export class MenuItem { */ setRightToLeft(rtl: boolean) { this.rightToLeft = rtl; + this.getElement()?.classList.toggle('blocklyMenuItemRtl', this.rightToLeft); } /** @@ -168,6 +165,12 @@ export class MenuItem { */ setCheckable(checkable: boolean) { this.checkable = checkable; + + if (!this.checkable) { + this.setChecked(false); + } + + this.toggleHasCheckbox(checkable); } /** @@ -177,7 +180,14 @@ export class MenuItem { * @internal */ setChecked(checked: boolean) { + if (checked && !this.checkable) return; + this.checked = checked; + const element = this.getElement(); + if (element) { + element.classList.toggle('blocklyMenuItemSelected', this.checked); + aria.setState(element, aria.State.SELECTED, this.checked); + } } /** @@ -188,20 +198,11 @@ export class MenuItem { */ setHighlighted(highlight: boolean) { this.highlight = highlight; - - const el = this.getElement(); - if (el && this.isEnabled()) { - // goog-menuitem-highlight is deprecated, use blocklyMenuItemHighlight. - // May 2020. - const name = 'blocklyMenuItemHighlight'; - const nameDep = 'goog-menuitem-highlight'; - if (highlight) { - dom.addClass(el, name); - dom.addClass(el, nameDep); - } else { - dom.removeClass(el, name); - dom.removeClass(el, nameDep); - } + if (this.isEnabled()) { + this.getElement()?.classList.toggle( + 'blocklyMenuItemHighlight', + this.highlight, + ); } } @@ -223,17 +224,25 @@ export class MenuItem { */ setEnabled(enabled: boolean) { this.enabled = enabled; + const element = this.getElement(); + if (element) { + element.classList.toggle('blocklyMenuItemDisabled', !this.enabled); + aria.setState(element, aria.State.DISABLED, !this.enabled); + } } /** * Performs the appropriate action when the menu item is activated * by the user. * + * @param menuSelectEvent the event that triggered the selection + * of the menu item. + * * @internal */ - performAction() { + performAction(menuSelectEvent: Event) { if (this.isEnabled() && this.actionHandler) { - this.actionHandler(this); + this.actionHandler(this, menuSelectEvent); } } @@ -245,7 +254,36 @@ export class MenuItem { * @param obj Used as the 'this' object in fn when called. * @internal */ - onAction(fn: (p1: MenuItem) => void, obj: object) { + onAction(fn: (p1: MenuItem, menuSelectEvent: Event) => void, obj: object) { this.actionHandler = fn.bind(obj); } + + /** + * Adds or removes the checkmark indicator on this menu item. + * The indicator is present even if this menu item is not checked, as long + * as it is checkable; its visibility is controlled with CSS. + * + * @param add True to add the checkmark indicator, false to remove it. + */ + private toggleHasCheckbox(add: boolean) { + if (add) { + if ( + this.getElement()?.querySelector( + '.blocklyMenuItemContent .blocklyMenuItemCheckbox', + ) + ) { + return; + } + + const checkbox = document.createElement('div'); + checkbox.className = 'blocklyMenuItemCheckbox '; + this.getElement() + ?.querySelector('.blocklyMenuItemContent') + ?.prepend(checkbox); + } else { + this.getElement() + ?.querySelector('.blocklyMenuItemContent .blocklyMenuItemCheckbox') + ?.remove(); + } + } } diff --git a/core/metrics_manager.ts b/packages/blockly/core/metrics_manager.ts similarity index 99% rename from core/metrics_manager.ts rename to packages/blockly/core/metrics_manager.ts index 62a2614b617..a8470462ce3 100644 --- a/core/metrics_manager.ts +++ b/packages/blockly/core/metrics_manager.ts @@ -76,7 +76,7 @@ export class MetricsManager implements IMetricsManager { * 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 MetricsManager#getFlyoutMetrics}. + * of a simple toolbox, see {@link (MetricsManager:class).getFlyoutMetrics}. * * @returns The object with the width, height and position of the toolbox. */ diff --git a/core/msg.ts b/packages/blockly/core/msg.ts similarity index 100% rename from core/msg.ts rename to packages/blockly/core/msg.ts diff --git a/core/names.ts b/packages/blockly/core/names.ts similarity index 96% rename from core/names.ts rename to packages/blockly/core/names.ts index 4f4c72faac8..db7486f719e 100644 --- a/core/names.ts +++ b/packages/blockly/core/names.ts @@ -11,9 +11,12 @@ */ // Former goog.module ID: Blockly.Names +import type {IVariableMap} from './interfaces/i_variable_map.js'; +import type { + IVariableModel, + IVariableState, +} from './interfaces/i_variable_model.js'; import {Msg} from './msg.js'; -// import * as Procedures from './procedures.js'; -import type {VariableMap} from './variable_map.js'; import * as Variables from './variables.js'; import type {Workspace} from './workspace.js'; @@ -39,7 +42,8 @@ export class Names { /** * The variable map from the workspace, containing Blockly variable models. */ - private variableMap: VariableMap | null = null; + private variableMap: IVariableMap> | null = + null; /** * @param reservedWordsList A comma-separated string of words that are illegal @@ -70,7 +74,7 @@ export class Names { * * @param map The map to track. */ - setVariableMap(map: VariableMap) { + setVariableMap(map: IVariableMap>) { this.variableMap = map; } @@ -95,7 +99,7 @@ export class Names { } const variable = this.variableMap.getVariableById(id); if (variable) { - return variable.name; + return variable.getName(); } return null; } diff --git a/packages/blockly/core/navigator.ts b/packages/blockly/core/navigator.ts new file mode 100644 index 00000000000..9c7c22f5987 --- /dev/null +++ b/packages/blockly/core/navigator.ts @@ -0,0 +1,123 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {IFocusableNode} from './interfaces/i_focusable_node.js'; +import type {INavigationPolicy} from './interfaces/i_navigation_policy.js'; +import {BlockCommentNavigationPolicy} from './keyboard_nav/block_comment_navigation_policy.js'; +import {BlockNavigationPolicy} from './keyboard_nav/block_navigation_policy.js'; +import {CommentBarButtonNavigationPolicy} from './keyboard_nav/comment_bar_button_navigation_policy.js'; +import {CommentEditorNavigationPolicy} from './keyboard_nav/comment_editor_navigation_policy.js'; +import {ConnectionNavigationPolicy} from './keyboard_nav/connection_navigation_policy.js'; +import {FieldNavigationPolicy} from './keyboard_nav/field_navigation_policy.js'; +import {IconNavigationPolicy} from './keyboard_nav/icon_navigation_policy.js'; +import {WorkspaceCommentNavigationPolicy} from './keyboard_nav/workspace_comment_navigation_policy.js'; +import {WorkspaceNavigationPolicy} from './keyboard_nav/workspace_navigation_policy.js'; + +type RuleList = INavigationPolicy[]; + +/** + * Class responsible for determining where focus should move in response to + * keyboard navigation commands. + */ +export class Navigator { + /** + * Map from classes to a corresponding ruleset to handle navigation from + * instances of that class. + */ + protected rules: RuleList = [ + new BlockNavigationPolicy(), + new FieldNavigationPolicy(), + new ConnectionNavigationPolicy(), + new WorkspaceNavigationPolicy(), + new IconNavigationPolicy(), + new WorkspaceCommentNavigationPolicy(), + new CommentBarButtonNavigationPolicy(), + new BlockCommentNavigationPolicy(), + new CommentEditorNavigationPolicy(), + ]; + + /** + * Adds a navigation ruleset to this Navigator. + * + * @param policy A ruleset that determines where focus should move starting + * from an instance of its managed class. + */ + addNavigationPolicy(policy: INavigationPolicy) { + this.rules.push(policy); + } + + /** + * Returns the navigation ruleset associated with the given object instance's + * class. + * + * @param current An object to retrieve a navigation ruleset for. + * @returns The navigation ruleset of objects of the given object's class, or + * undefined if no ruleset has been registered for the object's class. + */ + private get( + current: IFocusableNode, + ): INavigationPolicy | undefined { + return this.rules.find((rule) => rule.isApplicable(current)); + } + + /** + * Returns the first child of the given object instance, if any. + * + * @param current The object to retrieve the first child of. + * @returns The first child node of the given object, if any. + */ + getFirstChild(current: IFocusableNode): IFocusableNode | null { + const result = this.get(current)?.getFirstChild(current); + if (!result) return null; + if (!this.get(result)?.isNavigable(result)) { + return this.getFirstChild(result) || this.getNextSibling(result); + } + return result; + } + + /** + * Returns the parent of the given object instance, if any. + * + * @param current The object to retrieve the parent of. + * @returns The parent node of the given object, if any. + */ + getParent(current: IFocusableNode): IFocusableNode | null { + const result = this.get(current)?.getParent(current); + if (!result) return null; + if (!this.get(result)?.isNavigable(result)) return this.getParent(result); + return result; + } + + /** + * Returns the next sibling of the given object instance, if any. + * + * @param current The object to retrieve the next sibling node of. + * @returns The next sibling node of the given object, if any. + */ + getNextSibling(current: IFocusableNode): IFocusableNode | null { + const result = this.get(current)?.getNextSibling(current); + if (!result) return null; + if (!this.get(result)?.isNavigable(result)) { + return this.getNextSibling(result); + } + return result; + } + + /** + * Returns the previous sibling of the given object instance, if any. + * + * @param current The object to retrieve the previous sibling node of. + * @returns The previous sibling node of the given object, if any. + */ + getPreviousSibling(current: IFocusableNode): IFocusableNode | null { + const result = this.get(current)?.getPreviousSibling(current); + if (!result) return null; + if (!this.get(result)?.isNavigable(result)) { + return this.getPreviousSibling(result); + } + return result; + } +} diff --git a/core/observable_procedure_map.ts b/packages/blockly/core/observable_procedure_map.ts similarity index 100% rename from core/observable_procedure_map.ts rename to packages/blockly/core/observable_procedure_map.ts diff --git a/core/options.ts b/packages/blockly/core/options.ts similarity index 100% rename from core/options.ts rename to packages/blockly/core/options.ts diff --git a/core/positionable_helpers.ts b/packages/blockly/core/positionable_helpers.ts similarity index 100% rename from core/positionable_helpers.ts rename to packages/blockly/core/positionable_helpers.ts diff --git a/core/procedures.ts b/packages/blockly/core/procedures.ts similarity index 84% rename from core/procedures.ts rename to packages/blockly/core/procedures.ts index a16b0fce44a..73f06836cfe 100644 --- a/core/procedures.ts +++ b/packages/blockly/core/procedures.ts @@ -42,6 +42,8 @@ import {IProcedureModel} from './interfaces/i_procedure_model.js'; import {Msg} from './msg.js'; import {Names} from './names.js'; import {ObservableProcedureMap} from './observable_procedure_map.js'; +import * as deprecation from './utils/deprecation.js'; +import type {FlyoutItemInfo} from './utils/toolbox.js'; import * as utilsXml from './utils/xml.js'; import * as Variables from './variables.js'; import type {Workspace} from './workspace.js'; @@ -238,7 +240,7 @@ export function rename(this: Field, name: string): string { * @param workspace The workspace containing procedures. * @returns Array of XML block elements. */ -export function flyoutCategory(workspace: WorkspaceSvg): Element[] { +function xmlFlyoutCategory(workspace: WorkspaceSvg): Element[] { const xmlList = []; if (Blocks['procedures_defnoreturn']) { // @@ -322,6 +324,109 @@ export function flyoutCategory(workspace: WorkspaceSvg): Element[] { return xmlList; } +/** + * Internal wrapper that returns the contents of the procedure category. + * + * @internal + * @param workspace The workspace to populate procedure blocks for. + */ +export function internalFlyoutCategory( + workspace: WorkspaceSvg, +): FlyoutItemInfo[] { + return flyoutCategory(workspace, false); +} + +export function flyoutCategory( + workspace: WorkspaceSvg, + useXml: true, +): Element[]; +export function flyoutCategory( + workspace: WorkspaceSvg, + useXml: false, +): FlyoutItemInfo[]; +/** + * Construct the blocks required by the flyout for the procedure category. + * + * @param workspace The workspace containing procedures. + * @param useXml True to return the contents as XML, false to use JSON. + * @returns List of flyout contents as either XML or JSON. + */ +export function flyoutCategory( + workspace: WorkspaceSvg, + useXml = true, +): Element[] | FlyoutItemInfo[] { + if (useXml) { + deprecation.warn( + 'The XML return value of Blockly.Procedures.flyoutCategory()', + 'v12', + 'v13', + 'the same method, but handle a return type of FlyoutItemInfo[] (JSON) instead.', + ); + return xmlFlyoutCategory(workspace); + } + const blocks = []; + if (Blocks['procedures_defnoreturn']) { + blocks.push({ + 'kind': 'block', + 'type': 'procedures_defnoreturn', + 'gap': 16, + 'fields': { + 'NAME': Msg['PROCEDURES_DEFNORETURN_PROCEDURE'], + }, + }); + } + if (Blocks['procedures_defreturn']) { + blocks.push({ + 'kind': 'block', + 'type': 'procedures_defreturn', + 'gap': 16, + 'fields': { + 'NAME': Msg['PROCEDURES_DEFRETURN_PROCEDURE'], + }, + }); + } + if (Blocks['procedures_ifreturn']) { + blocks.push({ + 'kind': 'block', + 'type': 'procedures_ifreturn', + 'gap': 16, + }); + } + if (blocks.length) { + // Add slightly larger gap between system blocks and user calls. + blocks[blocks.length - 1]['gap'] = 24; + } + + /** + * Creates JSON block definitions for each of the given procedures. + * + * @param procedureList A list of procedures, each of which is defined by a + * three-element list of name, parameter list, and return value boolean. + * @param templateName The type of the block to generate. + */ + function populateProcedures( + procedureList: ProcedureTuple[], + templateName: string, + ) { + for (const [name, args] of procedureList) { + blocks.push({ + 'kind': 'block', + 'type': templateName, + 'gap': 16, + 'extraState': { + 'name': name, + 'params': args, + }, + }); + } + } + + const tuple = allProcedures(workspace); + populateProcedures(tuple[0], 'procedures_callnoreturn'); + populateProcedures(tuple[1], 'procedures_callreturn'); + return blocks; +} + /** * Updates the procedure mutator's flyout so that the arg block is not a * duplicate of another arg. diff --git a/core/registry.ts b/packages/blockly/core/registry.ts similarity index 94% rename from core/registry.ts rename to packages/blockly/core/registry.ts index 60e8049797c..4980a559478 100644 --- a/core/registry.ts +++ b/packages/blockly/core/registry.ts @@ -14,12 +14,19 @@ import type {IConnectionPreviewer} from './interfaces/i_connection_previewer.js' import type {ICopyData, ICopyable} from './interfaces/i_copyable.js'; import type {IDragger} from './interfaces/i_dragger.js'; import type {IFlyout} from './interfaces/i_flyout.js'; +import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js'; import type {IIcon} from './interfaces/i_icon.js'; import type {IMetricsManager} from './interfaces/i_metrics_manager.js'; import type {IPaster} from './interfaces/i_paster.js'; import type {ISerializer} from './interfaces/i_serializer.js'; import type {IToolbox} from './interfaces/i_toolbox.js'; -import type {Cursor} from './keyboard_nav/cursor.js'; +import type {IVariableMap} from './interfaces/i_variable_map.js'; +import type { + IVariableModel, + IVariableModelStatic, + IVariableState, +} from './interfaces/i_variable_model.js'; +import type {LineCursor} from './keyboard_nav/line_cursor.js'; import type {Options} from './options.js'; import type {Renderer} from './renderers/common/renderer.js'; import type {Theme} from './theme.js'; @@ -71,7 +78,7 @@ export class Type<_T> { 'connectionPreviewer', ); - static CURSOR = new Type('cursor'); + static CURSOR = new Type('cursor'); static EVENT = new Type('event'); @@ -93,6 +100,8 @@ export class Type<_T> { 'flyoutsHorizontalToolbox', ); + static FLYOUT_INFLATER = new Type('flyoutInflater'); + static METRICS_MANAGER = new Type('metricsManager'); /** @@ -109,6 +118,14 @@ export class Type<_T> { /** @internal */ static PASTER = new Type>>('paster'); + + static VARIABLE_MODEL = new Type< + IVariableModelStatic & IVariableModel + >('variableModel'); + + static VARIABLE_MAP = new Type>>( + 'variableMap', + ); } /** diff --git a/core/render_management.ts b/packages/blockly/core/render_management.ts similarity index 100% rename from core/render_management.ts rename to packages/blockly/core/render_management.ts diff --git a/core/rendered_connection.ts b/packages/blockly/core/rendered_connection.ts similarity index 84% rename from core/rendered_connection.ts rename to packages/blockly/core/rendered_connection.ts index c1d97dcddee..af1faa95870 100644 --- a/core/rendered_connection.ts +++ b/packages/blockly/core/rendered_connection.ts @@ -11,17 +11,22 @@ */ // Former goog.module ID: Blockly.RenderedConnection -import type {Block} from './block.js'; import type {BlockSvg} from './block_svg.js'; -import * as common from './common.js'; import {config} from './config.js'; import {Connection} from './connection.js'; import type {ConnectionDB} from './connection_db.js'; import {ConnectionType} from './connection_type.js'; +import * as ContextMenu from './contextmenu.js'; +import {ContextMenuRegistry} from './contextmenu_registry.js'; import * as eventUtils from './events/utils.js'; +import {IContextMenu} from './interfaces/i_contextmenu.js'; +import type {IFocusableNode} from './interfaces/i_focusable_node.js'; +import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; import {hasBubble} from './interfaces/i_has_bubble.js'; import * as internalConstants from './internal_constants.js'; import {Coordinate} from './utils/coordinate.js'; +import * as svgMath from './utils/svg_math.js'; +import {WorkspaceSvg} from './workspace_svg.js'; /** Maximum randomness in workspace units for bumping a block. */ const BUMP_RANDOMNESS = 10; @@ -29,7 +34,10 @@ const BUMP_RANDOMNESS = 10; /** * Class for a connection between blocks that may be rendered on screen. */ -export class RenderedConnection extends Connection { +export class RenderedConnection + extends Connection + implements IContextMenu, IFocusableNode +{ // TODO(b/109816955): remove '!', see go/strict-prop-init-fix. sourceBlock_!: BlockSvg; private readonly db: ConnectionDB; @@ -187,15 +195,12 @@ export class RenderedConnection extends Connection { ? inferiorRootBlock : superiorRootBlock; // Raise it to the top for extra visibility. - const selected = common.getSelected() === dynamicRootBlock; - if (!selected) dynamicRootBlock.addSelect(); if (dynamicRootBlock.RTL) { offsetX = -offsetX; } const dx = staticConnection.x + offsetX - dynamicConnection.x; const dy = staticConnection.y + offsetY - dynamicConnection.y; dynamicRootBlock.moveBy(dx, dy, ['bump']); - if (!selected) dynamicRootBlock.removeSelect(); } /** @@ -316,13 +321,28 @@ export class RenderedConnection extends Connection { /** Add highlighting around this connection. */ highlight() { this.highlighted = true; - this.getSourceBlock().queueRender(); + + // Note that this needs to be done synchronously (vs. queuing a render pass) + // since only a displayed element can be focused, and this focusable node is + // implemented to make itself visible immediately prior to receiving DOM + // focus. It's expected that the connection's position should already be + // correct by this point (otherwise it will be corrected in a subsequent + // draw pass). + const highlightSvg = this.findHighlightSvg(); + if (highlightSvg) { + highlightSvg.style.display = ''; + } } /** Remove the highlighting around this connection. */ unhighlight() { this.highlighted = false; - this.getSourceBlock().queueRender(); + + // Note that this is done synchronously for parity with highlight(). + const highlightSvg = this.findHighlightSvg(); + if (highlightSvg) { + highlightSvg.style.display = 'none'; + } } /** Returns true if this connection is highlighted, false otherwise. */ @@ -395,13 +415,13 @@ export class RenderedConnection extends Connection { * * @returns List of blocks to render. */ - startTrackingAll(): Block[] { + startTrackingAll(): BlockSvg[] { this.setTracking(true); // All blocks that are not tracked must start tracking before any // rendering takes place, since rendering requires knowing the dimensions // of lower blocks. Also, since rendering a block renders all its parents, // we only need to render the leaf nodes. - let renderList: Block[] = []; + let renderList: BlockSvg[] = []; if ( this.type !== ConnectionType.INPUT_VALUE && this.type !== ConnectionType.NEXT_STATEMENT @@ -533,21 +553,6 @@ export class RenderedConnection extends Connection { childBlock.updateDisabled(); childBlock.queueRender(); - // If either block being connected was selected, visually un- and reselect - // it. This has the effect of moving the selection path to the end of the - // list of child nodes in the DOM. Since SVG z-order is determined by node - // order in the DOM, this works around an issue where the selection outline - // path could be partially obscured by a new block inserted after it in the - // DOM. - const selection = common.getSelected(); - const selectedBlock = - (selection === parentBlock && parentBlock) || - (selection === childBlock && childBlock); - if (selectedBlock) { - selectedBlock.removeSelect(); - selectedBlock.addSelect(); - } - // The input the child block is connected to (if any). const parentInput = parentBlock.getInputWithBlock(childBlock); if (parentInput) { @@ -588,6 +593,78 @@ export class RenderedConnection extends Connection { this.sourceBlock_.queueRender(); return this; } + + /** + * Handles showing the context menu when it is opened on a connection. + * Note that typically the context menu can't be opened with the mouse + * on a connection, because you can't select a connection. But keyboard + * users may open the context menu with a keyboard shortcut. + * + * @param e Event that triggered the opening of the context menu. + */ + showContextMenu(e: Event): void { + const menuOptions = ContextMenuRegistry.registry.getContextMenuOptions( + {focusedNode: this}, + e, + ); + + if (!menuOptions.length) return; + + const block = this.getSourceBlock(); + const workspace = block.workspace; + + let location; + if (e instanceof PointerEvent) { + location = new Coordinate(e.clientX, e.clientY); + } else { + const connectionWSCoords = new Coordinate(this.x, this.y); + const connectionScreenCoords = svgMath.wsToScreenCoordinates( + workspace, + connectionWSCoords, + ); + location = connectionScreenCoords.translate(block.RTL ? -5 : 5, 5); + } + + ContextMenu.show(e, menuOptions, block.RTL, workspace, location); + } + + /** See IFocusableNode.getFocusableElement. */ + getFocusableElement(): HTMLElement | SVGElement { + const highlightSvg = this.findHighlightSvg(); + if (highlightSvg) return highlightSvg; + throw new Error('No highlight SVG found corresponding to this connection.'); + } + + /** See IFocusableNode.getFocusableTree. */ + getFocusableTree(): IFocusableTree { + return this.getSourceBlock().workspace as WorkspaceSvg; + } + + /** See IFocusableNode.onNodeFocus. */ + onNodeFocus(): void { + this.highlight(); + this.getSourceBlock().workspace.scrollBoundsIntoView( + this.getSourceBlock().getBoundingRectangleWithoutChildren(), + ); + } + + /** See IFocusableNode.onNodeBlur. */ + onNodeBlur(): void { + this.unhighlight(); + } + + /** See IFocusableNode.canBeFocused. */ + canBeFocused(): boolean { + return true; + } + + private findHighlightSvg(): SVGPathElement | null { + // This cast is valid as TypeScript's definition is wrong. See: + // https://github.com/microsoft/TypeScript/issues/60996. + return document.getElementById(this.id) as + | unknown + | null as SVGPathElement | null; + } } export namespace RenderedConnection { diff --git a/core/renderers/common/block_rendering.ts b/packages/blockly/core/renderers/common/block_rendering.ts similarity index 98% rename from core/renderers/common/block_rendering.ts rename to packages/blockly/core/renderers/common/block_rendering.ts index 27fbbd538c7..7ac27ae8b73 100644 --- a/core/renderers/common/block_rendering.ts +++ b/packages/blockly/core/renderers/common/block_rendering.ts @@ -33,7 +33,6 @@ import {Types} from '../measurables/types.js'; import {Drawer} from './drawer.js'; import type {IPathObject} from './i_path_object.js'; import {RenderInfo} from './info.js'; -import {MarkerSvg} from './marker_svg.js'; import {PathObject} from './path_object.js'; import {Renderer} from './renderer.js'; @@ -94,7 +93,6 @@ export { InRowSpacer, IPathObject, JaggedEdge, - MarkerSvg, Measurable, NextConnection, OutputConnection, diff --git a/core/renderers/common/constants.ts b/packages/blockly/core/renderers/common/constants.ts similarity index 95% rename from core/renderers/common/constants.ts rename to packages/blockly/core/renderers/common/constants.ts index 078fc01d648..c5a7a759c5c 100644 --- a/core/renderers/common/constants.ts +++ b/packages/blockly/core/renderers/common/constants.ts @@ -727,7 +727,10 @@ export class ConstantProvider { svgPaths.point(70, -height), svgPaths.point(width, 0), ]); - return {height, width, path: mainPath}; + // Height is actually the Y position of the control points defining the + // curve of the hat; the hat's actual rendered height is 3/4 of the control + // points' Y position, per https://stackoverflow.com/a/5327329 + return {height: height * 0.75, width, path: mainPath}; } /** @@ -923,8 +926,18 @@ export class ConstantProvider { * @param svg The root of the workspace's SVG. * @param tagName The name to use for the CSS style tag. * @param selector The CSS selector to use. + * @param injectionDivIfIsParent The div containing the parent workspace and + * all related workspaces and block containers, if this renderer is for the + * parent workspace. CSS variables representing SVG patterns will be scoped + * to this container. Child workspaces should not override the CSS variables + * created by the parent and thus do not need access to the injection div. */ - createDom(svg: SVGElement, tagName: string, selector: string) { + createDom( + svg: SVGElement, + tagName: string, + selector: string, + injectionDivIfIsParent?: HTMLElement, + ) { this.injectCSS_(tagName, selector); /* @@ -1031,6 +1044,24 @@ export class ConstantProvider { this.disabledPattern = disabledPattern; this.createDebugFilter(); + + if (injectionDivIfIsParent) { + // If this renderer is for the parent workspace, add CSS variables scoped + // to the injection div referencing the created patterns so that CSS can + // apply the patterns to any element in the injection div. + injectionDivIfIsParent.style.setProperty( + '--blocklyEmbossFilter', + `url(#${this.embossFilterId})`, + ); + injectionDivIfIsParent.style.setProperty( + '--blocklyDisabledPattern', + `url(#${this.disabledPatternId})`, + ); + injectionDivIfIsParent.style.setProperty( + '--blocklyDebugFilter', + `url(#${this.debugFilterId})`, + ); + } } /** @@ -1132,14 +1163,14 @@ export class ConstantProvider { `${selector} .blocklyText {`, `fill: #fff;`, `}`, - `${selector} .blocklyNonEditableText>rect,`, - `${selector} .blocklyEditableText>rect {`, + `${selector} .blocklyNonEditableField>rect,`, + `${selector} .blocklyEditableField>rect {`, `fill: ${this.FIELD_BORDER_RECT_COLOUR};`, `fill-opacity: .6;`, `stroke: none;`, `}`, - `${selector} .blocklyNonEditableText>text,`, - `${selector} .blocklyEditableText>text {`, + `${selector} .blocklyNonEditableField>text,`, + `${selector} .blocklyEditableField>text {`, `fill: #000;`, `}`, @@ -1154,7 +1185,7 @@ export class ConstantProvider { `}`, // Editable field hover. - `${selector} .blocklyEditableText:not(.editing):hover>rect {`, + `${selector} .blocklyEditableField:not(.blocklyEditing):hover>rect {`, `stroke: #fff;`, `stroke-width: 2;`, `}`, diff --git a/core/renderers/common/drawer.ts b/packages/blockly/core/renderers/common/drawer.ts similarity index 94% rename from core/renderers/common/drawer.ts rename to packages/blockly/core/renderers/common/drawer.ts index 59a856011f2..c474bc8c339 100644 --- a/core/renderers/common/drawer.ts +++ b/packages/blockly/core/renderers/common/drawer.ts @@ -15,7 +15,6 @@ import type {ExternalValueInput} from '../measurables/external_value_input.js'; import type {Field} from '../measurables/field.js'; import type {Icon} from '../measurables/icon.js'; import type {InlineInput} from '../measurables/inline_input.js'; -import type {PreviousConnection} from '../measurables/previous_connection.js'; import type {Row} from '../measurables/row.js'; import {Types} from '../measurables/types.js'; import type {ConstantProvider, Notch, PuzzleTab} from './constants.js'; @@ -116,21 +115,19 @@ export class Drawer { this.outlinePath_ += this.constants_.OUTSIDE_CORNERS.topLeft; } else if (Types.isRightRoundedCorner(elem)) { this.outlinePath_ += this.constants_.OUTSIDE_CORNERS.topRight; - } else if ( - Types.isPreviousConnection(elem) && - elem instanceof Connection - ) { - this.outlinePath_ += ( - (elem as PreviousConnection).shape as Notch - ).pathLeft; + } else if (Types.isPreviousConnection(elem)) { + this.outlinePath_ += (elem.shape as Notch).pathLeft; } else if (Types.isHat(elem)) { this.outlinePath_ += this.constants_.START_HAT.path; } else if (Types.isSpacer(elem)) { this.outlinePath_ += svgPaths.lineOnAxis('h', elem.width); } + // No branch for a square corner, because it's a no-op. } - // No branch for a square corner, because it's a no-op. - this.outlinePath_ += svgPaths.lineOnAxis('v', topRow.height); + this.outlinePath_ += svgPaths.lineOnAxis( + 'v', + topRow.height - topRow.ascenderHeight, + ); } /** @@ -217,7 +214,7 @@ export class Drawer { let rightCornerYOffset = 0; let outlinePath = ''; for (let i = elems.length - 1, elem; (elem = elems[i]); i--) { - if (Types.isNextConnection(elem) && elem instanceof Connection) { + if (Types.isNextConnection(elem)) { outlinePath += (elem.shape as Notch).pathRight; } else if (Types.isLeftSquareCorner(elem)) { outlinePath += svgPaths.lineOnAxis('H', bottomRow.xPos); @@ -269,9 +266,9 @@ export class Drawer { for (let i = 0, row; (row = this.info_.rows[i]); i++) { for (let j = 0, elem; (elem = row.elements[j]); j++) { if (Types.isInlineInput(elem)) { - this.drawInlineInput_(elem as InlineInput); + this.drawInlineInput_(elem); } else if (Types.isIcon(elem) || Types.isField(elem)) { - this.layoutField_(elem as Field | Icon); + this.layoutField_(elem); } } } @@ -295,13 +292,13 @@ export class Drawer { } if (Types.isIcon(fieldInfo)) { - const icon = (fieldInfo as Icon).icon; + const icon = fieldInfo.icon; icon.setOffsetInBlock(new Coordinate(xPos, yPos)); if (this.info_.isInsertionMarker) { icon.hideForInsertionMarker(); } } else { - const svgGroup = (fieldInfo as Field).field.getSvgRoot()!; + const svgGroup = fieldInfo.field.getSvgRoot()!; svgGroup.setAttribute( 'transform', 'translate(' + xPos + ',' + yPos + ')' + scale, @@ -441,19 +438,16 @@ export class Drawer { for (const elem of row.elements) { if (!(elem instanceof Connection)) continue; - if (elem.highlighted) { - this.drawConnectionHighlightPath(elem); - } else { - this.block_.pathObject.removeConnectionHighlight?.( - elem.connectionModel, - ); + const highlightSvg = this.drawConnectionHighlightPath(elem); + if (highlightSvg) { + highlightSvg.style.display = elem.highlighted ? '' : 'none'; } } } } /** Returns a path to highlight the given connection. */ - drawConnectionHighlightPath(measurable: Connection) { + drawConnectionHighlightPath(measurable: Connection): SVGElement | undefined { const conn = measurable.connectionModel; let path = ''; if ( @@ -465,7 +459,7 @@ export class Drawer { path = this.getStatementConnectionHighlightPath(measurable); } const block = conn.getSourceBlock(); - block.pathObject.addConnectionHighlight?.( + return block.pathObject.addConnectionHighlight?.( conn, path, conn.getOffsetInBlock(), diff --git a/core/renderers/common/i_path_object.ts b/packages/blockly/core/renderers/common/i_path_object.ts similarity index 76% rename from core/renderers/common/i_path_object.ts rename to packages/blockly/core/renderers/common/i_path_object.ts index 3a78035e156..a68c3a41148 100644 --- a/core/renderers/common/i_path_object.ts +++ b/packages/blockly/core/renderers/common/i_path_object.ts @@ -30,18 +30,6 @@ export interface IPathObject { /** The primary path of the block. */ style: BlockStyle; - /** - * Holds the cursors SVG element when the cursor is attached to the block. - * This is null if there is no cursor on the block. - */ - cursorSvg: SVGElement | null; - - /** - * Holds the markers SVG element when the marker is attached to the block. - * This is null if there is no marker on the block. - */ - markerSvg: SVGElement | null; - /** * Set the path generated by the renderer onto the respective SVG element. * @@ -49,42 +37,11 @@ export interface IPathObject { */ setPath(pathString: string): void; - /** - * Apply the stored colours to the block's path, taking into account whether - * the paths belong to a shadow block. - * - * @param block The source block. - */ - applyColour(block: BlockSvg): void; - - /** - * Update the style. - * - * @param blockStyle The block style to use. - */ - setStyle(blockStyle: BlockStyle): void; - /** * Flip the SVG paths in RTL. */ flipRTL(): void; - /** - * 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. - */ - setCursorSvg(cursorSvg: SVGElement): void; - - /** - * 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. - */ - setMarkerSvg(markerSvg: SVGElement): void; - /** * Set whether the block shows a highlight or not. Block highlighting is * often used to visually mark blocks currently being executed. @@ -128,10 +85,25 @@ export interface IPathObject { connectionPath: string, offset: Coordinate, rtl: boolean, - ): void; + ): SVGElement; + + /** + * Apply the stored colours to the block's path, taking into account whether + * the paths belong to a shadow block. + * + * @param block The source block. + */ + applyColour?(block: BlockSvg): void; /** * Removes any highlight associated with the given connection, if it exists. */ removeConnectionHighlight?(connection: RenderedConnection): void; + + /** + * Update the style. + * + * @param blockStyle The block style to use. + */ + setStyle?(blockStyle: BlockStyle): void; } diff --git a/core/renderers/common/info.ts b/packages/blockly/core/renderers/common/info.ts similarity index 98% rename from core/renderers/common/info.ts rename to packages/blockly/core/renderers/common/info.ts index ff073ace48b..0e4d3e9460c 100644 --- a/core/renderers/common/info.ts +++ b/packages/blockly/core/renderers/common/info.ts @@ -231,7 +231,6 @@ export class RenderInfo { if (hasHat) { const hat = new Hat(this.constants_); this.topRow.elements.push(hat); - this.topRow.capline = hat.ascenderHeight; } else if (hasPrevious) { this.topRow.hasPreviousConnection = true; this.topRow.connection = new PreviousConnection( @@ -458,6 +457,11 @@ export class RenderInfo { } } + // Don't add padding after zero-width fields. + if (prev && Types.isField(prev) && prev.width === 0) { + return this.constants_.NO_PADDING; + } + return this.constants_.MEDIUM_PADDING; } @@ -672,20 +676,17 @@ export class RenderInfo { return row.yPos + elem.height / 2; } if (Types.isBottomRow(row)) { - const bottomRow = row as BottomRow; - const baseline = - bottomRow.yPos + bottomRow.height - bottomRow.descenderHeight; + const baseline = row.yPos + row.height - row.descenderHeight; if (Types.isNextConnection(elem)) { return baseline + elem.height / 2; } return baseline - elem.height / 2; } if (Types.isTopRow(row)) { - const topRow = row as TopRow; if (Types.isHat(elem)) { - return topRow.capline - elem.height / 2; + return row.capline - elem.height / 2; } - return topRow.capline + elem.height / 2; + return row.capline + elem.height / 2; } return row.yPos + row.height / 2; } diff --git a/core/renderers/common/path_object.ts b/packages/blockly/core/renderers/common/path_object.ts similarity index 74% rename from core/renderers/common/path_object.ts rename to packages/blockly/core/renderers/common/path_object.ts index 0f46cf3a423..f6291b9f0fa 100644 --- a/core/renderers/common/path_object.ts +++ b/packages/blockly/core/renderers/common/path_object.ts @@ -24,18 +24,6 @@ export class PathObject implements IPathObject { svgRoot: SVGElement; svgPath: SVGElement; - /** - * Holds the cursors svg element when the cursor is attached to the block. - * This is null if there is no cursor on the block. - */ - cursorSvg: SVGElement | null = null; - - /** - * Holds the markers svg element when the marker is attached to the block. - * This is null if there is no marker on the block. - */ - markerSvg: SVGElement | null = null; - constants: ConstantProvider; style: BlockStyle; @@ -65,6 +53,8 @@ export class PathObject implements IPathObject { {'class': 'blocklyPath'}, this.svgRoot, ); + + this.setClass_('blocklyBlock', true); } /** @@ -84,42 +74,6 @@ export class PathObject implements IPathObject { this.svgPath.setAttribute('transform', 'scale(-1 1)'); } - /** - * 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. - */ - setCursorSvg(cursorSvg: SVGElement) { - if (!cursorSvg) { - this.cursorSvg = null; - return; - } - - this.svgRoot.appendChild(cursorSvg); - this.cursorSvg = 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. - */ - setMarkerSvg(markerSvg: SVGElement) { - if (!markerSvg) { - this.markerSvg = null; - return; - } - - if (this.cursorSvg) { - this.svgRoot.insertBefore(markerSvg, this.cursorSvg); - } else { - this.svgRoot.appendChild(markerSvg); - } - this.markerSvg = markerSvg; - } - /** * Apply the stored colours to the block's path, taking into account whether * the paths belong to a shadow block. @@ -167,14 +121,12 @@ export class PathObject implements IPathObject { * * @param enable True if highlighted. */ + updateHighlighted(enable: boolean) { if (enable) { - this.svgPath.setAttribute( - 'filter', - 'url(#' + this.constants.embossFilterId + ')', - ); + this.setClass_('blocklyHighlighted', true); } else { - this.svgPath.setAttribute('filter', 'none'); + this.setClass_('blocklyHighlighted', false); } } @@ -185,8 +137,11 @@ export class PathObject implements IPathObject { */ protected updateShadow_(shadow: boolean) { if (shadow) { + this.setClass_('blocklyShadow', true); this.svgPath.setAttribute('stroke', 'none'); this.svgPath.setAttribute('fill', this.style.colourSecondary); + } else { + this.setClass_('blocklyShadow', false); } } @@ -197,12 +152,7 @@ export class PathObject implements IPathObject { */ protected updateDisabled_(disabled: boolean) { this.setClass_('blocklyDisabled', disabled); - if (disabled) { - this.svgPath.setAttribute( - 'fill', - 'url(#' + this.constants.disabledPatternId + ')', - ); - } + this.setClass_('blocklyDisabledPattern', disabled); } /** @@ -270,37 +220,32 @@ export class PathObject implements IPathObject { connectionPath: string, offset: Coordinate, rtl: boolean, - ) { - if (this.connectionHighlights.has(connection)) { - if (this.currentHighlightMatchesNew(connection, connectionPath, offset)) { - return; - } - this.removeConnectionHighlight(connection); + ): SVGElement { + const transformation = + `translate(${offset.x}, ${offset.y})` + (rtl ? ' scale(-1 1)' : ''); + + const previousHighlight = this.connectionHighlights.get(connection); + if (previousHighlight) { + // Since a connection already exists, make sure that its path and + // transform are correct. + previousHighlight.setAttribute('d', connectionPath); + previousHighlight.setAttribute('transform', transformation); + return previousHighlight; } const highlight = dom.createSvgElement( Svg.PATH, { + 'id': connection.id, 'class': 'blocklyHighlightedConnectionPath', + 'style': 'display: none;', 'd': connectionPath, - 'transform': - `translate(${offset.x}, ${offset.y})` + (rtl ? ' scale(-1 1)' : ''), + 'transform': transformation, }, this.svgRoot, ); this.connectionHighlights.set(connection, highlight); - } - - private currentHighlightMatchesNew( - connection: RenderedConnection, - newPath: string, - newOffset: Coordinate, - ): boolean { - const currPath = this.connectionHighlights - .get(connection) - ?.getAttribute('d'); - const currOffset = this.highlightOffsets.get(connection); - return currPath === newPath && Coordinate.equals(currOffset, newOffset); + return highlight; } /** diff --git a/core/renderers/common/renderer.ts b/packages/blockly/core/renderers/common/renderer.ts similarity index 72% rename from core/renderers/common/renderer.ts rename to packages/blockly/core/renderers/common/renderer.ts index d3bff56a702..5b7e687c25e 100644 --- a/core/renderers/common/renderer.ts +++ b/packages/blockly/core/renderers/common/renderer.ts @@ -10,21 +10,12 @@ import type {Block} from '../../block.js'; import type {BlockSvg} from '../../block_svg.js'; import {Connection} from '../../connection.js'; import {ConnectionType} from '../../connection_type.js'; -import { - InsertionMarkerManager, - PreviewType, -} from '../../insertion_marker_manager.js'; import type {IRegistrable} from '../../interfaces/i_registrable.js'; -import type {Marker} from '../../keyboard_nav/marker.js'; -import type {RenderedConnection} from '../../rendered_connection.js'; import type {BlockStyle, Theme} from '../../theme.js'; -import * as deprecation from '../../utils/deprecation.js'; -import type {WorkspaceSvg} from '../../workspace_svg.js'; import {ConstantProvider} from './constants.js'; import {Drawer} from './drawer.js'; import type {IPathObject} from './i_path_object.js'; import {RenderInfo} from './info.js'; -import {MarkerSvg} from './marker_svg.js'; import {PathObject} from './path_object.js'; /** @@ -79,17 +70,27 @@ export class Renderer implements IRegistrable { /** * Create any DOM elements that this renderer needs. * If you need to create additional DOM elements, override the - * {@link ConstantProvider#createDom} method instead. + * {@link blockRendering#ConstantProvider.createDom} method instead. * * @param svg The root of the workspace's SVG. * @param theme The workspace theme object. + * @param injectionDivIfIsParent The div containing the parent workspace and + * all related workspaces and block containers, if this renderer is for the + * parent workspace. CSS variables representing SVG patterns will be scoped + * to this container. Child workspaces should not override the CSS variables + * created by the parent and thus do not need access to the injection div. * @internal */ - createDom(svg: SVGElement, theme: Theme) { + createDom( + svg: SVGElement, + theme: Theme, + injectionDivIfIsParent?: HTMLElement, + ) { this.constants_.createDom( svg, this.name + '-' + theme.name, '.' + this.getClassName() + '.' + theme.getClassName(), + injectionDivIfIsParent, ); } @@ -98,8 +99,17 @@ export class Renderer implements IRegistrable { * * @param svg The root of the workspace's SVG. * @param theme The workspace theme object. - */ - refreshDom(svg: SVGElement, theme: Theme) { + * @param injectionDivIfIsParent The div containing the parent workspace and + * all related workspaces and block containers, if this renderer is for the + * parent workspace. CSS variables representing SVG patterns will be scoped + * to this container. Child workspaces should not override the CSS variables + * created by the parent and thus do not need access to the injection div. + */ + refreshDom( + svg: SVGElement, + theme: Theme, + injectionDivIfIsParent?: HTMLElement, + ) { const previousConstants = this.getConstants(); previousConstants.dispose(); this.constants_ = this.makeConstants_(); @@ -110,7 +120,7 @@ export class Renderer implements IRegistrable { this.constants_.randomIdentifier = previousConstants.randomIdentifier; this.constants_.setTheme(theme); this.constants_.init(); - this.createDom(svg, theme); + this.createDom(svg, theme, injectionDivIfIsParent); } /** @@ -154,17 +164,6 @@ export class Renderer implements IRegistrable { return new Drawer(block, info); } - /** - * Create a new instance of the renderer's marker drawer. - * - * @param workspace The workspace the marker belongs to. - * @param marker The marker. - * @returns The object in charge of drawing the marker. - */ - makeMarkerDrawer(workspace: WorkspaceSvg, marker: Marker): MarkerSvg { - return new MarkerSvg(workspace, this.getConstants(), marker); - } - /** * Create a new instance of a renderer path object. * @@ -223,49 +222,6 @@ export class Renderer implements IRegistrable { ); } - /** - * Chooses a connection preview method based on the available connection, the - * current dragged connection, and the block being dragged. - * - * @param closest The available connection. - * @param local The connection currently being dragged. - * @param topBlock The block currently being dragged. - * @returns The preview type to display. - * - * @deprecated v10 - This function is no longer respected. A custom - * IConnectionPreviewer may be able to fulfill the functionality. - */ - getConnectionPreviewMethod( - closest: RenderedConnection, - local: RenderedConnection, - topBlock: BlockSvg, - ): PreviewType { - deprecation.warn( - 'getConnectionPreviewMethod', - 'v10', - 'v12', - 'an IConnectionPreviewer, if it fulfills your use case.', - ); - if ( - local.type === ConnectionType.OUTPUT_VALUE || - local.type === ConnectionType.PREVIOUS_STATEMENT - ) { - if ( - !closest.isConnected() || - this.orphanCanConnectAtEnd( - topBlock, - closest.targetBlock() as BlockSvg, - local.type, - ) - ) { - return InsertionMarkerManager.PREVIEW_TYPE.INSERTION_MARKER; - } - return InsertionMarkerManager.PREVIEW_TYPE.REPLACEMENT_FADE; - } - - return InsertionMarkerManager.PREVIEW_TYPE.INSERTION_MARKER; - } - /** * Render the block. * diff --git a/core/renderers/geras/constants.ts b/packages/blockly/core/renderers/geras/constants.ts similarity index 100% rename from core/renderers/geras/constants.ts rename to packages/blockly/core/renderers/geras/constants.ts diff --git a/core/renderers/geras/drawer.ts b/packages/blockly/core/renderers/geras/drawer.ts similarity index 98% rename from core/renderers/geras/drawer.ts rename to packages/blockly/core/renderers/geras/drawer.ts index 542b21ff93b..9d0ed829be7 100644 --- a/core/renderers/geras/drawer.ts +++ b/packages/blockly/core/renderers/geras/drawer.ts @@ -100,7 +100,7 @@ export class Drawer extends BaseDrawer { } override drawInlineInput_(input: InlineInput) { - this.highlighter_.drawInlineInput(input as InlineInput); + this.highlighter_.drawInlineInput(input); super.drawInlineInput_(input); } diff --git a/core/renderers/geras/geras.ts b/packages/blockly/core/renderers/geras/geras.ts similarity index 100% rename from core/renderers/geras/geras.ts rename to packages/blockly/core/renderers/geras/geras.ts diff --git a/core/renderers/geras/highlight_constants.ts b/packages/blockly/core/renderers/geras/highlight_constants.ts similarity index 100% rename from core/renderers/geras/highlight_constants.ts rename to packages/blockly/core/renderers/geras/highlight_constants.ts diff --git a/core/renderers/geras/highlighter.ts b/packages/blockly/core/renderers/geras/highlighter.ts similarity index 100% rename from core/renderers/geras/highlighter.ts rename to packages/blockly/core/renderers/geras/highlighter.ts diff --git a/core/renderers/geras/info.ts b/packages/blockly/core/renderers/geras/info.ts similarity index 95% rename from core/renderers/geras/info.ts rename to packages/blockly/core/renderers/geras/info.ts index b9cc1c59c8c..11f9e764ac6 100644 --- a/core/renderers/geras/info.ts +++ b/packages/blockly/core/renderers/geras/info.ts @@ -14,13 +14,9 @@ import {StatementInput} from '../../inputs/statement_input.js'; import {ValueInput} from '../../inputs/value_input.js'; import {RenderInfo as BaseRenderInfo} from '../common/info.js'; import type {Measurable} from '../measurables/base.js'; -import type {BottomRow} from '../measurables/bottom_row.js'; import {ExternalValueInput} from '../measurables/external_value_input.js'; -import type {Field} from '../measurables/field.js'; import {InRowSpacer} from '../measurables/in_row_spacer.js'; -import type {InputRow} from '../measurables/input_row.js'; import type {Row} from '../measurables/row.js'; -import type {TopRow} from '../measurables/top_row.js'; import {Types} from '../measurables/types.js'; import type {ConstantProvider} from './constants.js'; import {InlineInput} from './measurables/inline_input.js'; @@ -150,7 +146,7 @@ export class RenderInfo extends BaseRenderInfo { override getInRowSpacing_(prev: Measurable | null, next: Measurable | null) { if (!prev) { // Between an editable field and the beginning of the row. - if (next && Types.isField(next) && (next as Field).isEditable) { + if (next && Types.isField(next) && next.isEditable) { return this.constants_.MEDIUM_PADDING; } // Inline input at the beginning of the row. @@ -167,7 +163,10 @@ export class RenderInfo extends BaseRenderInfo { // Spacing between a non-input and the end of the row or a statement input. if (!Types.isInput(prev) && (!next || Types.isStatementInput(next))) { // Between an editable field and the end of the row. - if (Types.isField(prev) && (prev as Field).isEditable) { + if (Types.isField(prev) && prev.isEditable) { + if (prev.width === 0) { + return this.constants_.NO_PADDING; + } return this.constants_.MEDIUM_PADDING; } // Padding at the end of an icon-only row to make the block shape clearer. @@ -208,7 +207,7 @@ export class RenderInfo extends BaseRenderInfo { // Spacing between a non-input and an input. if (!Types.isInput(prev) && next && Types.isInput(next)) { // Between an editable field and an input. - if (Types.isField(prev) && (prev as Field).isEditable) { + if (Types.isField(prev) && prev.isEditable) { if (Types.isInlineInput(next)) { return this.constants_.SMALL_PADDING; } else if (Types.isExternalInput(next)) { @@ -233,7 +232,7 @@ export class RenderInfo extends BaseRenderInfo { // Spacing between an inline input and a field. if (Types.isInlineInput(prev) && next && Types.isField(next)) { // Editable field after inline input. - if ((next as Field).isEditable) { + if (next.isEditable) { return this.constants_.MEDIUM_PADDING; } else { // Noneditable field after inline input. @@ -278,8 +277,11 @@ export class RenderInfo extends BaseRenderInfo { Types.isField(prev) && next && Types.isField(next) && - (prev as Field).isEditable === (next as Field).isEditable + prev.isEditable === next.isEditable ) { + if (prev.width === 0) { + return this.constants_.NO_PADDING; + } return this.constants_.LARGE_PADDING; } @@ -323,20 +325,17 @@ export class RenderInfo extends BaseRenderInfo { return row.yPos + elem.height / 2; } if (Types.isBottomRow(row)) { - const bottomRow = row as BottomRow; - const baseline = - bottomRow.yPos + bottomRow.height - bottomRow.descenderHeight; + const baseline = row.yPos + row.height - row.descenderHeight; if (Types.isNextConnection(elem)) { return baseline + elem.height / 2; } return baseline - elem.height / 2; } if (Types.isTopRow(row)) { - const topRow = row as TopRow; if (Types.isHat(elem)) { - return topRow.capline - elem.height / 2; + return row.capline - elem.height / 2; } - return topRow.capline + elem.height / 2; + return row.capline + elem.height / 2; } let result = row.yPos; @@ -370,7 +369,7 @@ export class RenderInfo extends BaseRenderInfo { rowNextRightEdges.set(row, nextRightEdge); if (Types.isInputRow(row)) { if (row.hasStatement) { - this.alignStatementRow_(row as InputRow); + this.alignStatementRow_(row); } if ( prevInput && diff --git a/core/renderers/geras/measurables/inline_input.ts b/packages/blockly/core/renderers/geras/measurables/inline_input.ts similarity index 100% rename from core/renderers/geras/measurables/inline_input.ts rename to packages/blockly/core/renderers/geras/measurables/inline_input.ts diff --git a/core/renderers/geras/measurables/statement_input.ts b/packages/blockly/core/renderers/geras/measurables/statement_input.ts similarity index 100% rename from core/renderers/geras/measurables/statement_input.ts rename to packages/blockly/core/renderers/geras/measurables/statement_input.ts diff --git a/core/renderers/geras/path_object.ts b/packages/blockly/core/renderers/geras/path_object.ts similarity index 83% rename from core/renderers/geras/path_object.ts rename to packages/blockly/core/renderers/geras/path_object.ts index c1d689535af..88e5bc57870 100644 --- a/core/renderers/geras/path_object.ts +++ b/packages/blockly/core/renderers/geras/path_object.ts @@ -81,12 +81,6 @@ export class PathObject extends BasePathObject { override applyColour(block: BlockSvg) { this.svgPathLight.style.display = ''; this.svgPathDark.style.display = ''; - if (!this.style.colourTertiary) { - throw new Error( - 'The renderer did not properly initialize the tertiary colour of ' + - 'the block style', - ); - } this.svgPathLight.setAttribute('stroke', this.style.colourTertiary); this.svgPathDark.setAttribute('fill', this.colourDark); @@ -102,30 +96,19 @@ export class PathObject extends BasePathObject { } override updateHighlighted(highlighted: boolean) { + super.updateHighlighted(highlighted); if (highlighted) { - this.svgPath.setAttribute( - 'filter', - 'url(#' + this.constants.embossFilterId + ')', - ); this.svgPathLight.style.display = 'none'; } else { - this.svgPath.setAttribute('filter', 'none'); this.svgPathLight.style.display = 'inline'; } } override updateShadow_(shadow: boolean) { + super.updateShadow_(shadow); if (shadow) { this.svgPathLight.style.display = 'none'; - if (!this.style.colourSecondary) { - throw new Error( - 'The renderer did not properly initialize the secondary colour ' + - 'of the block style block style', - ); - } this.svgPathDark.setAttribute('fill', this.style.colourSecondary); - this.svgPath.setAttribute('stroke', 'none'); - this.svgPath.setAttribute('fill', this.style.colourSecondary); } } diff --git a/core/renderers/geras/renderer.ts b/packages/blockly/core/renderers/geras/renderer.ts similarity index 96% rename from core/renderers/geras/renderer.ts rename to packages/blockly/core/renderers/geras/renderer.ts index aba8fc3eab1..ade8e5039d4 100644 --- a/core/renderers/geras/renderer.ts +++ b/packages/blockly/core/renderers/geras/renderer.ts @@ -49,8 +49,12 @@ export class Renderer extends BaseRenderer { this.highlightConstants.init(); } - override refreshDom(svg: SVGElement, theme: Theme) { - super.refreshDom(svg, theme); + override refreshDom( + svg: SVGElement, + theme: Theme, + injectionDiv: HTMLElement, + ) { + super.refreshDom(svg, theme, injectionDiv); this.getHighlightConstants().init(); } diff --git a/core/renderers/measurables/base.ts b/packages/blockly/core/renderers/measurables/base.ts similarity index 100% rename from core/renderers/measurables/base.ts rename to packages/blockly/core/renderers/measurables/base.ts diff --git a/core/renderers/measurables/bottom_row.ts b/packages/blockly/core/renderers/measurables/bottom_row.ts similarity index 100% rename from core/renderers/measurables/bottom_row.ts rename to packages/blockly/core/renderers/measurables/bottom_row.ts diff --git a/core/renderers/measurables/connection.ts b/packages/blockly/core/renderers/measurables/connection.ts similarity index 100% rename from core/renderers/measurables/connection.ts rename to packages/blockly/core/renderers/measurables/connection.ts diff --git a/core/renderers/measurables/external_value_input.ts b/packages/blockly/core/renderers/measurables/external_value_input.ts similarity index 100% rename from core/renderers/measurables/external_value_input.ts rename to packages/blockly/core/renderers/measurables/external_value_input.ts diff --git a/core/renderers/measurables/field.ts b/packages/blockly/core/renderers/measurables/field.ts similarity index 100% rename from core/renderers/measurables/field.ts rename to packages/blockly/core/renderers/measurables/field.ts diff --git a/core/renderers/measurables/hat.ts b/packages/blockly/core/renderers/measurables/hat.ts similarity index 100% rename from core/renderers/measurables/hat.ts rename to packages/blockly/core/renderers/measurables/hat.ts diff --git a/core/renderers/measurables/icon.ts b/packages/blockly/core/renderers/measurables/icon.ts similarity index 100% rename from core/renderers/measurables/icon.ts rename to packages/blockly/core/renderers/measurables/icon.ts diff --git a/core/renderers/measurables/in_row_spacer.ts b/packages/blockly/core/renderers/measurables/in_row_spacer.ts similarity index 61% rename from core/renderers/measurables/in_row_spacer.ts rename to packages/blockly/core/renderers/measurables/in_row_spacer.ts index ec64e71a23a..d9378620cf7 100644 --- a/core/renderers/measurables/in_row_spacer.ts +++ b/packages/blockly/core/renderers/measurables/in_row_spacer.ts @@ -15,6 +15,14 @@ import {Types} from './types.js'; * row. */ export class InRowSpacer extends Measurable { + // This field exists solely to structurally distinguish this type from other + // Measurable subclasses. Because this class otherwise has the same fields as + // Measurable, and Typescript doesn't support nominal typing, Typescript will + // consider it and other subclasses in the same situation as being of the same + // type, even if typeguards are used, which could result in Typescript typing + // objects of this class as `never`. + private inRowSpacer: undefined; + /** * @param constants The rendering constants provider. * @param width The width of the spacer. diff --git a/core/renderers/measurables/inline_input.ts b/packages/blockly/core/renderers/measurables/inline_input.ts similarity index 100% rename from core/renderers/measurables/inline_input.ts rename to packages/blockly/core/renderers/measurables/inline_input.ts diff --git a/core/renderers/measurables/input_connection.ts b/packages/blockly/core/renderers/measurables/input_connection.ts similarity index 100% rename from core/renderers/measurables/input_connection.ts rename to packages/blockly/core/renderers/measurables/input_connection.ts diff --git a/core/renderers/measurables/input_row.ts b/packages/blockly/core/renderers/measurables/input_row.ts similarity index 81% rename from core/renderers/measurables/input_row.ts rename to packages/blockly/core/renderers/measurables/input_row.ts index a9924246f38..869e6718f03 100644 --- a/core/renderers/measurables/input_row.ts +++ b/packages/blockly/core/renderers/measurables/input_row.ts @@ -7,10 +7,7 @@ // Former goog.module ID: Blockly.blockRendering.InputRow import type {ConstantProvider} from '../common/constants.js'; -import {ExternalValueInput} from './external_value_input.js'; -import {InputConnection} from './input_connection.js'; import {Row} from './row.js'; -import {StatementInput} from './statement_input.js'; import {Types} from './types.js'; /** @@ -40,12 +37,11 @@ export class InputRow extends Row { for (let i = 0; i < this.elements.length; i++) { const elem = this.elements[i]; this.width += elem.width; - if (Types.isInput(elem) && elem instanceof InputConnection) { - if (Types.isStatementInput(elem) && elem instanceof StatementInput) { + if (Types.isInput(elem)) { + if (Types.isStatementInput(elem)) { connectedBlockWidths += elem.connectedBlockWidth; } else if ( Types.isExternalInput(elem) && - elem instanceof ExternalValueInput && elem.connectedBlockWidth !== 0 ) { connectedBlockWidths += diff --git a/core/renderers/measurables/jagged_edge.ts b/packages/blockly/core/renderers/measurables/jagged_edge.ts similarity index 61% rename from core/renderers/measurables/jagged_edge.ts rename to packages/blockly/core/renderers/measurables/jagged_edge.ts index daca2512118..982e2b3530c 100644 --- a/core/renderers/measurables/jagged_edge.ts +++ b/packages/blockly/core/renderers/measurables/jagged_edge.ts @@ -15,6 +15,14 @@ import {Types} from './types.js'; * collapsed block takes up during rendering. */ export class JaggedEdge extends Measurable { + // This field exists solely to structurally distinguish this type from other + // Measurable subclasses. Because this class otherwise has the same fields as + // Measurable, and Typescript doesn't support nominal typing, Typescript will + // consider it and other subclasses in the same situation as being of the same + // type, even if typeguards are used, which could result in Typescript typing + // objects of this class as `never`. + private jaggedEdge: undefined; + /** * @param constants The rendering constants provider. */ diff --git a/core/renderers/measurables/next_connection.ts b/packages/blockly/core/renderers/measurables/next_connection.ts similarity index 66% rename from core/renderers/measurables/next_connection.ts rename to packages/blockly/core/renderers/measurables/next_connection.ts index ea22001ed53..c10a26904bc 100644 --- a/core/renderers/measurables/next_connection.ts +++ b/packages/blockly/core/renderers/measurables/next_connection.ts @@ -16,6 +16,14 @@ import {Types} from './types.js'; * up during rendering. */ export class NextConnection extends Connection { + // This field exists solely to structurally distinguish this type from other + // Measurable subclasses. Because this class otherwise has the same fields as + // Measurable, and Typescript doesn't support nominal typing, Typescript will + // consider it and other subclasses in the same situation as being of the same + // type, even if typeguards are used, which could result in Typescript typing + // objects of this class as `never`. + private nextConnection: undefined; + /** * @param constants The rendering constants provider. * @param connectionModel The connection object on the block that this diff --git a/core/renderers/measurables/output_connection.ts b/packages/blockly/core/renderers/measurables/output_connection.ts similarity index 100% rename from core/renderers/measurables/output_connection.ts rename to packages/blockly/core/renderers/measurables/output_connection.ts diff --git a/core/renderers/measurables/previous_connection.ts b/packages/blockly/core/renderers/measurables/previous_connection.ts similarity index 66% rename from core/renderers/measurables/previous_connection.ts rename to packages/blockly/core/renderers/measurables/previous_connection.ts index 1314eb6a45d..30944766c48 100644 --- a/core/renderers/measurables/previous_connection.ts +++ b/packages/blockly/core/renderers/measurables/previous_connection.ts @@ -16,6 +16,14 @@ import {Types} from './types.js'; * up during rendering. */ export class PreviousConnection extends Connection { + // This field exists solely to structurally distinguish this type from other + // Measurable subclasses. Because this class otherwise has the same fields as + // Measurable, and Typescript doesn't support nominal typing, Typescript will + // consider it and other subclasses in the same situation as being of the same + // type, even if typeguards are used, which could result in Typescript typing + // objects of this class as `never`. + private previousConnection: undefined; + /** * @param constants The rendering constants provider. * @param connectionModel The connection object on the block that this diff --git a/core/renderers/measurables/round_corner.ts b/packages/blockly/core/renderers/measurables/round_corner.ts similarity index 68% rename from core/renderers/measurables/round_corner.ts rename to packages/blockly/core/renderers/measurables/round_corner.ts index 60bbed70784..02c90546e1d 100644 --- a/core/renderers/measurables/round_corner.ts +++ b/packages/blockly/core/renderers/measurables/round_corner.ts @@ -15,6 +15,14 @@ import {Types} from './types.js'; * during rendering. */ export class RoundCorner extends Measurable { + // This field exists solely to structurally distinguish this type from other + // Measurable subclasses. Because this class otherwise has the same fields as + // Measurable, and Typescript doesn't support nominal typing, Typescript will + // consider it and other subclasses in the same situation as being of the same + // type, even if typeguards are used, which could result in Typescript typing + // objects of this class as `never`. + private roundCorner: undefined; + /** * @param constants The rendering constants provider. * @param opt_position The position of this corner. diff --git a/core/renderers/measurables/row.ts b/packages/blockly/core/renderers/measurables/row.ts similarity index 95% rename from core/renderers/measurables/row.ts rename to packages/blockly/core/renderers/measurables/row.ts index 613ec6ace74..bc4707e83af 100644 --- a/core/renderers/measurables/row.ts +++ b/packages/blockly/core/renderers/measurables/row.ts @@ -127,7 +127,7 @@ export class Row { for (let i = this.elements.length - 1; i >= 0; i--) { const elem = this.elements[i]; if (Types.isInput(elem)) { - return elem as InputConnection; + return elem; } } return null; @@ -166,8 +166,8 @@ export class Row { getFirstSpacer(): InRowSpacer | null { for (let i = 0; i < this.elements.length; i++) { const elem = this.elements[i]; - if (Types.isSpacer(elem)) { - return elem as InRowSpacer; + if (Types.isInRowSpacer(elem)) { + return elem; } } return null; @@ -181,8 +181,8 @@ export class Row { getLastSpacer(): InRowSpacer | null { for (let i = this.elements.length - 1; i >= 0; i--) { const elem = this.elements[i]; - if (Types.isSpacer(elem)) { - return elem as InRowSpacer; + if (Types.isInRowSpacer(elem)) { + return elem; } } return null; diff --git a/core/renderers/measurables/spacer_row.ts b/packages/blockly/core/renderers/measurables/spacer_row.ts similarity index 100% rename from core/renderers/measurables/spacer_row.ts rename to packages/blockly/core/renderers/measurables/spacer_row.ts diff --git a/core/renderers/measurables/square_corner.ts b/packages/blockly/core/renderers/measurables/square_corner.ts similarity index 65% rename from core/renderers/measurables/square_corner.ts rename to packages/blockly/core/renderers/measurables/square_corner.ts index 29749ac057d..054e148be23 100644 --- a/core/renderers/measurables/square_corner.ts +++ b/packages/blockly/core/renderers/measurables/square_corner.ts @@ -15,6 +15,14 @@ import {Types} from './types.js'; * during rendering. */ export class SquareCorner extends Measurable { + // This field exists solely to structurally distinguish this type from other + // Measurable subclasses. Because this class otherwise has the same fields as + // Measurable, and Typescript doesn't support nominal typing, Typescript will + // consider it and other subclasses in the same situation as being of the same + // type, even if typeguards are used, which could result in Typescript typing + // objects of this class as `never`. + private squareCorner: undefined; + /** * @param constants The rendering constants provider. * @param opt_position The position of this corner. diff --git a/core/renderers/measurables/statement_input.ts b/packages/blockly/core/renderers/measurables/statement_input.ts similarity index 72% rename from core/renderers/measurables/statement_input.ts rename to packages/blockly/core/renderers/measurables/statement_input.ts index 91fe5b64a45..b0b527d36dd 100644 --- a/core/renderers/measurables/statement_input.ts +++ b/packages/blockly/core/renderers/measurables/statement_input.ts @@ -16,6 +16,14 @@ import {Types} from './types.js'; * during rendering */ export class StatementInput extends InputConnection { + // This field exists solely to structurally distinguish this type from other + // Measurable subclasses. Because this class otherwise has the same fields as + // Measurable, and Typescript doesn't support nominal typing, Typescript will + // consider it and other subclasses in the same situation as being of the same + // type, even if typeguards are used, which could result in Typescript typing + // objects of this class as `never`. + private statementInput: undefined; + /** * @param constants The rendering constants provider. * @param input The statement input to measure and store information for. diff --git a/core/renderers/measurables/top_row.ts b/packages/blockly/core/renderers/measurables/top_row.ts similarity index 97% rename from core/renderers/measurables/top_row.ts rename to packages/blockly/core/renderers/measurables/top_row.ts index b87ce4ad753..f1e7794806d 100644 --- a/core/renderers/measurables/top_row.ts +++ b/packages/blockly/core/renderers/measurables/top_row.ts @@ -8,7 +8,6 @@ import type {BlockSvg} from '../../block_svg.js'; import type {ConstantProvider} from '../common/constants.js'; -import {Hat} from './hat.js'; import type {PreviousConnection} from './previous_connection.js'; import {Row} from './row.js'; import {Types} from './types.js'; @@ -85,7 +84,7 @@ export class TopRow extends Row { const elem = this.elements[i]; width += elem.width; if (!Types.isSpacer(elem)) { - if (Types.isHat(elem) && elem instanceof Hat) { + if (Types.isHat(elem)) { ascenderHeight = Math.max(ascenderHeight, elem.ascenderHeight); } else { height = Math.max(height, elem.height); diff --git a/core/renderers/measurables/types.ts b/packages/blockly/core/renderers/measurables/types.ts similarity index 64% rename from core/renderers/measurables/types.ts rename to packages/blockly/core/renderers/measurables/types.ts index a145b156303..99de339f1b1 100644 --- a/core/renderers/measurables/types.ts +++ b/packages/blockly/core/renderers/measurables/types.ts @@ -7,7 +7,24 @@ // Former goog.module ID: Blockly.blockRendering.Types import type {Measurable} from './base.js'; +import type {BottomRow} from './bottom_row.js'; +import type {ExternalValueInput} from './external_value_input.js'; +import type {Field} from './field.js'; +import type {Hat} from './hat.js'; +import type {Icon} from './icon.js'; +import type {InRowSpacer} from './in_row_spacer.js'; +import type {InlineInput} from './inline_input.js'; +import type {InputConnection} from './input_connection.js'; +import type {InputRow} from './input_row.js'; +import type {JaggedEdge} from './jagged_edge.js'; +import type {NextConnection} from './next_connection.js'; +import type {PreviousConnection} from './previous_connection.js'; +import type {RoundCorner} from './round_corner.js'; import type {Row} from './row.js'; +import type {SpacerRow} from './spacer_row.js'; +import type {SquareCorner} from './square_corner.js'; +import type {StatementInput} from './statement_input.js'; +import type {TopRow} from './top_row.js'; /** * Types of rendering elements. @@ -82,8 +99,8 @@ class TypesContainer { * @param elem The element to check. * @returns 1 if the object stores information about a field. */ - isField(elem: Measurable): number { - return elem.type & this.FIELD; + isField(elem: Measurable): elem is Field { + return (elem.type & this.FIELD) >= 1; } /** @@ -92,8 +109,8 @@ class TypesContainer { * @param elem The element to check. * @returns 1 if the object stores information about a hat. */ - isHat(elem: Measurable): number { - return elem.type & this.HAT; + isHat(elem: Measurable): elem is Hat { + return (elem.type & this.HAT) >= 1; } /** @@ -102,8 +119,8 @@ class TypesContainer { * @param elem The element to check. * @returns 1 if the object stores information about an icon. */ - isIcon(elem: Measurable): number { - return elem.type & this.ICON; + isIcon(elem: Measurable): elem is Icon { + return (elem.type & this.ICON) >= 1; } /** @@ -112,8 +129,8 @@ class TypesContainer { * @param elem The element to check. * @returns 1 if the object stores information about a spacer. */ - isSpacer(elem: Measurable | Row): number { - return elem.type & this.SPACER; + isSpacer(elem: Measurable | Row): elem is SpacerRow | InRowSpacer { + return (elem.type & this.SPACER) >= 1; } /** @@ -122,8 +139,18 @@ class TypesContainer { * @param elem The element to check. * @returns 1 if the object stores information about an in-row spacer. */ - isInRowSpacer(elem: Measurable): number { - return elem.type & this.IN_ROW_SPACER; + isInRowSpacer(elem: Measurable): elem is InRowSpacer { + return (elem.type & this.IN_ROW_SPACER) >= 1; + } + + /** + * Whether a row is a spacer row. + * + * @param row The row to check. + * @returns True if the row is a spacer row. + */ + isSpacerRow(row: Row): row is SpacerRow { + return (row.type & this.BETWEEN_ROW_SPACER) >= 1; } /** @@ -132,8 +159,8 @@ class TypesContainer { * @param elem The element to check. * @returns 1 if the object stores information about an input. */ - isInput(elem: Measurable): number { - return elem.type & this.INPUT; + isInput(elem: Measurable): elem is InputConnection { + return (elem.type & this.INPUT) >= 1; } /** @@ -142,8 +169,8 @@ class TypesContainer { * @param elem The element to check. * @returns 1 if the object stores information about an external input. */ - isExternalInput(elem: Measurable): number { - return elem.type & this.EXTERNAL_VALUE_INPUT; + isExternalInput(elem: Measurable): elem is ExternalValueInput { + return (elem.type & this.EXTERNAL_VALUE_INPUT) >= 1; } /** @@ -152,8 +179,8 @@ class TypesContainer { * @param elem The element to check. * @returns 1 if the object stores information about an inline input. */ - isInlineInput(elem: Measurable): number { - return elem.type & this.INLINE_INPUT; + isInlineInput(elem: Measurable): elem is InlineInput { + return (elem.type & this.INLINE_INPUT) >= 1; } /** @@ -162,8 +189,8 @@ class TypesContainer { * @param elem The element to check. * @returns 1 if the object stores information about a statement input. */ - isStatementInput(elem: Measurable): number { - return elem.type & this.STATEMENT_INPUT; + isStatementInput(elem: Measurable): elem is StatementInput { + return (elem.type & this.STATEMENT_INPUT) >= 1; } /** @@ -172,8 +199,8 @@ class TypesContainer { * @param elem The element to check. * @returns 1 if the object stores information about a previous connection. */ - isPreviousConnection(elem: Measurable): number { - return elem.type & this.PREVIOUS_CONNECTION; + isPreviousConnection(elem: Measurable): elem is PreviousConnection { + return (elem.type & this.PREVIOUS_CONNECTION) >= 1; } /** @@ -182,8 +209,8 @@ class TypesContainer { * @param elem The element to check. * @returns 1 if the object stores information about a next connection. */ - isNextConnection(elem: Measurable): number { - return elem.type & this.NEXT_CONNECTION; + isNextConnection(elem: Measurable): elem is NextConnection { + return (elem.type & this.NEXT_CONNECTION) >= 1; } /** @@ -194,8 +221,17 @@ class TypesContainer { * @returns 1 if the object stores information about a previous or next * connection. */ - isPreviousOrNextConnection(elem: Measurable): number { - return elem.type & (this.PREVIOUS_CONNECTION | this.NEXT_CONNECTION); + isPreviousOrNextConnection( + elem: Measurable, + ): elem is PreviousConnection | NextConnection { + return this.isPreviousConnection(elem) || this.isNextConnection(elem); + } + + isRoundCorner(elem: Measurable): elem is RoundCorner { + return ( + (elem.type & this.LEFT_ROUND_CORNER) >= 1 || + (elem.type & this.RIGHT_ROUND_CORNER) >= 1 + ); } /** @@ -204,8 +240,10 @@ class TypesContainer { * @param elem The element to check. * @returns 1 if the object stores information about a left round corner. */ - isLeftRoundedCorner(elem: Measurable): number { - return elem.type & this.LEFT_ROUND_CORNER; + isLeftRoundedCorner(elem: Measurable): boolean { + return ( + this.isRoundCorner(elem) && (elem.type & this.LEFT_ROUND_CORNER) >= 1 + ); } /** @@ -214,8 +252,10 @@ class TypesContainer { * @param elem The element to check. * @returns 1 if the object stores information about a right round corner. */ - isRightRoundedCorner(elem: Measurable): number { - return elem.type & this.RIGHT_ROUND_CORNER; + isRightRoundedCorner(elem: Measurable): boolean { + return ( + this.isRoundCorner(elem) && (elem.type & this.RIGHT_ROUND_CORNER) >= 1 + ); } /** @@ -224,8 +264,8 @@ class TypesContainer { * @param elem The element to check. * @returns 1 if the object stores information about a left square corner. */ - isLeftSquareCorner(elem: Measurable): number { - return elem.type & this.LEFT_SQUARE_CORNER; + isLeftSquareCorner(elem: Measurable): boolean { + return (elem.type & this.LEFT_SQUARE_CORNER) >= 1; } /** @@ -234,8 +274,8 @@ class TypesContainer { * @param elem The element to check. * @returns 1 if the object stores information about a right square corner. */ - isRightSquareCorner(elem: Measurable): number { - return elem.type & this.RIGHT_SQUARE_CORNER; + isRightSquareCorner(elem: Measurable): boolean { + return (elem.type & this.RIGHT_SQUARE_CORNER) >= 1; } /** @@ -244,8 +284,8 @@ class TypesContainer { * @param elem The element to check. * @returns 1 if the object stores information about a corner. */ - isCorner(elem: Measurable): number { - return elem.type & this.CORNER; + isCorner(elem: Measurable): elem is SquareCorner | RoundCorner { + return (elem.type & this.CORNER) >= 1; } /** @@ -254,8 +294,8 @@ class TypesContainer { * @param elem The element to check. * @returns 1 if the object stores information about a jagged edge. */ - isJaggedEdge(elem: Measurable): number { - return elem.type & this.JAGGED_EDGE; + isJaggedEdge(elem: Measurable): elem is JaggedEdge { + return (elem.type & this.JAGGED_EDGE) >= 1; } /** @@ -264,8 +304,8 @@ class TypesContainer { * @param row The row to check. * @returns 1 if the object stores information about a row. */ - isRow(row: Row): number { - return row.type & this.ROW; + isRow(row: Row): row is Row { + return (row.type & this.ROW) >= 1; } /** @@ -274,8 +314,8 @@ class TypesContainer { * @param row The row to check. * @returns 1 if the object stores information about a between-row spacer. */ - isBetweenRowSpacer(row: Row): number { - return row.type & this.BETWEEN_ROW_SPACER; + isBetweenRowSpacer(row: Row): row is SpacerRow { + return (row.type & this.BETWEEN_ROW_SPACER) >= 1; } /** @@ -284,8 +324,8 @@ class TypesContainer { * @param row The row to check. * @returns 1 if the object stores information about a top row. */ - isTopRow(row: Row): number { - return row.type & this.TOP_ROW; + isTopRow(row: Row): row is TopRow { + return (row.type & this.TOP_ROW) >= 1; } /** @@ -294,8 +334,8 @@ class TypesContainer { * @param row The row to check. * @returns 1 if the object stores information about a bottom row. */ - isBottomRow(row: Row): number { - return row.type & this.BOTTOM_ROW; + isBottomRow(row: Row): row is BottomRow { + return (row.type & this.BOTTOM_ROW) >= 1; } /** @@ -304,8 +344,8 @@ class TypesContainer { * @param row The row to check. * @returns 1 if the object stores information about a top or bottom row. */ - isTopOrBottomRow(row: Row): number { - return row.type & (this.TOP_ROW | this.BOTTOM_ROW); + isTopOrBottomRow(row: Row): row is TopRow | BottomRow { + return this.isTopRow(row) || this.isBottomRow(row); } /** @@ -314,8 +354,8 @@ class TypesContainer { * @param row The row to check. * @returns 1 if the object stores information about an input row. */ - isInputRow(row: Row): number { - return row.type & this.INPUT_ROW; + isInputRow(row: Row): row is InputRow { + return (row.type & this.INPUT_ROW) >= 1; } } diff --git a/core/renderers/thrasos/info.ts b/packages/blockly/core/renderers/thrasos/info.ts similarity index 93% rename from core/renderers/thrasos/info.ts rename to packages/blockly/core/renderers/thrasos/info.ts index 23772a9af0e..62c08fa424d 100644 --- a/core/renderers/thrasos/info.ts +++ b/packages/blockly/core/renderers/thrasos/info.ts @@ -9,11 +9,8 @@ import type {BlockSvg} from '../../block_svg.js'; import {RenderInfo as BaseRenderInfo} from '../common/info.js'; import type {Measurable} from '../measurables/base.js'; -import type {BottomRow} from '../measurables/bottom_row.js'; -import type {Field} from '../measurables/field.js'; import {InRowSpacer} from '../measurables/in_row_spacer.js'; import type {Row} from '../measurables/row.js'; -import type {TopRow} from '../measurables/top_row.js'; import {Types} from '../measurables/types.js'; import type {Renderer} from './renderer.js'; @@ -94,7 +91,7 @@ export class RenderInfo extends BaseRenderInfo { override getInRowSpacing_(prev: Measurable | null, next: Measurable | null) { if (!prev) { // Between an editable field and the beginning of the row. - if (next && Types.isField(next) && (next as Field).isEditable) { + if (next && Types.isField(next) && next.isEditable) { return this.constants_.MEDIUM_PADDING; } // Inline input at the beginning of the row. @@ -111,7 +108,10 @@ export class RenderInfo extends BaseRenderInfo { // Spacing between a non-input and the end of the row. if (!Types.isInput(prev) && !next) { // Between an editable field and the end of the row. - if (Types.isField(prev) && (prev as Field).isEditable) { + if (Types.isField(prev) && prev.isEditable) { + if (prev.width === 0) { + return this.constants_.NO_PADDING; + } return this.constants_.MEDIUM_PADDING; } // Padding at the end of an icon-only row to make the block shape clearer. @@ -151,7 +151,7 @@ export class RenderInfo extends BaseRenderInfo { // Spacing between a non-input and an input. if (!Types.isInput(prev) && next && Types.isInput(next)) { // Between an editable field and an input. - if (Types.isField(prev) && (prev as Field).isEditable) { + if (Types.isField(prev) && prev.isEditable) { if (Types.isInlineInput(next)) { return this.constants_.SMALL_PADDING; } else if (Types.isExternalInput(next)) { @@ -177,7 +177,7 @@ export class RenderInfo extends BaseRenderInfo { // Spacing between an inline input and a field. if (Types.isInlineInput(prev) && next && Types.isField(next)) { // Editable field after inline input. - if ((next as Field).isEditable) { + if (next.isEditable) { return this.constants_.MEDIUM_PADDING; } else { // Noneditable field after inline input. @@ -205,8 +205,11 @@ export class RenderInfo extends BaseRenderInfo { Types.isField(prev) && next && Types.isField(next) && - (prev as Field).isEditable === (next as Field).isEditable + prev.isEditable === next.isEditable ) { + if (prev.width === 0) { + return this.constants_.NO_PADDING; + } return this.constants_.LARGE_PADDING; } @@ -247,20 +250,17 @@ export class RenderInfo extends BaseRenderInfo { return row.yPos + elem.height / 2; } if (Types.isBottomRow(row)) { - const bottomRow = row as BottomRow; - const baseline = - bottomRow.yPos + bottomRow.height - bottomRow.descenderHeight; + const baseline = row.yPos + row.height - row.descenderHeight; if (Types.isNextConnection(elem)) { return baseline + elem.height / 2; } return baseline - elem.height / 2; } if (Types.isTopRow(row)) { - const topRow = row as TopRow; if (Types.isHat(elem)) { - return topRow.capline - elem.height / 2; + return row.capline - elem.height / 2; } - return topRow.capline + elem.height / 2; + return row.capline + elem.height / 2; } let result = row.yPos; diff --git a/core/renderers/thrasos/renderer.ts b/packages/blockly/core/renderers/thrasos/renderer.ts similarity index 100% rename from core/renderers/thrasos/renderer.ts rename to packages/blockly/core/renderers/thrasos/renderer.ts diff --git a/core/renderers/thrasos/thrasos.ts b/packages/blockly/core/renderers/thrasos/thrasos.ts similarity index 100% rename from core/renderers/thrasos/thrasos.ts rename to packages/blockly/core/renderers/thrasos/thrasos.ts diff --git a/core/renderers/zelos/constants.ts b/packages/blockly/core/renderers/zelos/constants.ts similarity index 92% rename from core/renderers/zelos/constants.ts rename to packages/blockly/core/renderers/zelos/constants.ts index afef605ebb3..8cd36e02589 100644 --- a/core/renderers/zelos/constants.ts +++ b/packages/blockly/core/renderers/zelos/constants.ts @@ -151,9 +151,19 @@ export class ConstantProvider extends BaseConstantProvider { */ SQUARED: Shape | null = null; - constructor() { + /** + * Creates a new ConstantProvider. + * + * @param gridUnit If set, defines the base unit used to calculate other + * constants. + */ + constructor(gridUnit?: number) { super(); + if (gridUnit) { + this.GRID_UNIT = gridUnit; + } + this.SMALL_PADDING = this.GRID_UNIT; this.MEDIUM_PADDING = 2 * this.GRID_UNIT; @@ -290,7 +300,10 @@ export class ConstantProvider extends BaseConstantProvider { svgPaths.point(71, -height), svgPaths.point(width, 0), ]); - return {height, width, path: mainPath}; + // Height is actually the Y position of the control points defining the + // curve of the hat; the hat's actual rendered height is 3/4 of the control + // points' Y position, per https://stackoverflow.com/a/5327329 + return {height: height * 0.75, width, path: mainPath}; } /** @@ -662,8 +675,13 @@ export class ConstantProvider extends BaseConstantProvider { return utilsColour.blend('#000', colour, 0.25) || colour; } - override createDom(svg: SVGElement, tagName: string, selector: string) { - super.createDom(svg, tagName, selector); + override createDom( + svg: SVGElement, + tagName: string, + selector: string, + injectionDivIfIsParent?: HTMLElement, + ) { + super.createDom(svg, tagName, selector, injectionDivIfIsParent); /* ... filters go here ... @@ -782,6 +800,20 @@ export class ConstantProvider extends BaseConstantProvider { ); this.replacementGlowFilterId = replacementGlowFilter.id; this.replacementGlowFilter = replacementGlowFilter; + + if (injectionDivIfIsParent) { + // If this renderer is for the parent workspace, add CSS variables scoped + // to the injection div referencing the created patterns so that CSS can + // apply the patterns to any element in the injection div. + injectionDivIfIsParent.style.setProperty( + '--blocklySelectedGlowFilter', + `url(#${this.selectedGlowFilterId})`, + ); + injectionDivIfIsParent.style.setProperty( + '--blocklyReplacementGlowFilter', + `url(#${this.replacementGlowFilterId})`, + ); + } } override getCSS_(selector: string) { @@ -801,14 +833,14 @@ export class ConstantProvider extends BaseConstantProvider { `${selector} .blocklyText {`, `fill: #fff;`, `}`, - `${selector} .blocklyNonEditableText>rect:not(.blocklyDropdownRect),`, - `${selector} .blocklyEditableText>rect:not(.blocklyDropdownRect) {`, + `${selector} .blocklyNonEditableField>rect:not(.blocklyDropdownRect),`, + `${selector} .blocklyEditableField>rect:not(.blocklyDropdownRect) {`, `fill: ${this.FIELD_BORDER_RECT_COLOUR};`, `}`, - `${selector} .blocklyNonEditableText>text,`, - `${selector} .blocklyEditableText>text,`, - `${selector} .blocklyNonEditableText>g>text,`, - `${selector} .blocklyEditableText>g>text {`, + `${selector} .blocklyNonEditableField>text,`, + `${selector} .blocklyEditableField>text,`, + `${selector} .blocklyNonEditableField>g>text,`, + `${selector} .blocklyEditableField>g>text {`, `fill: #575E75;`, `}`, @@ -824,9 +856,9 @@ export class ConstantProvider extends BaseConstantProvider { // Editable field hover. `${selector} .blocklyDraggable:not(.blocklyDisabled)`, - ` .blocklyEditableText:not(.editing):hover>rect,`, + ` .blocklyEditableField:not(.blocklyEditing):hover>rect,`, `${selector} .blocklyDraggable:not(.blocklyDisabled)`, - ` .blocklyEditableText:not(.editing):hover>.blocklyPath {`, + ` .blocklyEditableField:not(.blocklyEditing):hover>.blocklyPath {`, `stroke: #fff;`, `stroke-width: 2;`, `}`, @@ -858,8 +890,8 @@ export class ConstantProvider extends BaseConstantProvider { `}`, // Disabled outline paths. - `${selector} .blocklyDisabled > .blocklyOutlinePath {`, - `fill: url(#blocklyDisabledPattern${this.randomIdentifier})`, + `${selector} .blocklyDisabledPattern > .blocklyOutlinePath {`, + `fill: var(--blocklyDisabledPattern)`, `}`, // Insertion marker. @@ -867,6 +899,15 @@ export class ConstantProvider extends BaseConstantProvider { `fill-opacity: ${this.INSERTION_MARKER_OPACITY};`, `stroke: none;`, `}`, + + `${selector} .blocklySelected>.blocklyPath.blocklyPathSelected {`, + `fill: none;`, + `filter: var(--blocklySelectedGlowFilter);`, + `}`, + + `${selector} .blocklyReplaceable>.blocklyPath {`, + `filter: var(--blocklyReplacementGlowFilter);`, + `}`, ]; } } diff --git a/core/renderers/zelos/drawer.ts b/packages/blockly/core/renderers/zelos/drawer.ts similarity index 93% rename from core/renderers/zelos/drawer.ts rename to packages/blockly/core/renderers/zelos/drawer.ts index e5b91c1e607..b38711eb6c3 100644 --- a/core/renderers/zelos/drawer.ts +++ b/packages/blockly/core/renderers/zelos/drawer.ts @@ -15,7 +15,6 @@ import {Connection} from '../measurables/connection.js'; import type {InlineInput} from '../measurables/inline_input.js'; import {OutputConnection} from '../measurables/output_connection.js'; import type {Row} from '../measurables/row.js'; -import type {SpacerRow} from '../measurables/spacer_row.js'; import {Types} from '../measurables/types.js'; import type {InsideCorners} from './constants.js'; import type {RenderInfo} from './info.js'; @@ -96,20 +95,19 @@ export class Drawer extends BaseDrawer { return; } if (Types.isSpacer(row)) { - const spacerRow = row as SpacerRow; - const precedesStatement = spacerRow.precedesStatement; - const followsStatement = spacerRow.followsStatement; + const precedesStatement = row.precedesStatement; + const followsStatement = row.followsStatement; if (precedesStatement || followsStatement) { const insideCorners = this.constants_.INSIDE_CORNERS as InsideCorners; const cornerHeight = insideCorners.rightHeight; const remainingHeight = - spacerRow.height - (precedesStatement ? cornerHeight : 0); + row.height - (precedesStatement ? cornerHeight : 0); const bottomRightPath = followsStatement ? insideCorners.pathBottomRight : ''; const verticalPath = remainingHeight > 0 - ? svgPaths.lineOnAxis('V', spacerRow.yPos + remainingHeight) + ? svgPaths.lineOnAxis('V', row.yPos + remainingHeight) : ''; const topRightPath = precedesStatement ? insideCorners.pathTopRight @@ -236,15 +234,16 @@ export class Drawer extends BaseDrawer { } /** Returns a path to highlight the given connection. */ - drawConnectionHighlightPath(measurable: Connection) { + override drawConnectionHighlightPath( + measurable: Connection, + ): SVGElement | undefined { const conn = measurable.connectionModel; if ( conn.type === ConnectionType.NEXT_STATEMENT || conn.type === ConnectionType.PREVIOUS_STATEMENT || (conn.type === ConnectionType.OUTPUT_VALUE && !measurable.isDynamicShape) ) { - super.drawConnectionHighlightPath(measurable); - return; + return super.drawConnectionHighlightPath(measurable); } let path = ''; @@ -263,7 +262,7 @@ export class Drawer extends BaseDrawer { (output.shape as DynamicShape).pathDown(output.height); } const block = conn.getSourceBlock(); - block.pathObject.addConnectionHighlight?.( + return block.pathObject.addConnectionHighlight?.( conn, path, conn.getOffsetInBlock(), diff --git a/core/renderers/zelos/info.ts b/packages/blockly/core/renderers/zelos/info.ts similarity index 97% rename from core/renderers/zelos/info.ts rename to packages/blockly/core/renderers/zelos/info.ts index dd3702fe5d1..e14c584f0dc 100644 --- a/core/renderers/zelos/info.ts +++ b/packages/blockly/core/renderers/zelos/info.ts @@ -20,7 +20,6 @@ import {RenderInfo as BaseRenderInfo} from '../common/info.js'; import type {Measurable} from '../measurables/base.js'; import {Field} from '../measurables/field.js'; import {InRowSpacer} from '../measurables/in_row_spacer.js'; -import {InputConnection} from '../measurables/input_connection.js'; import type {Row} from '../measurables/row.js'; import type {SpacerRow} from '../measurables/spacer_row.js'; import {Types} from '../measurables/types.js'; @@ -187,6 +186,12 @@ export class RenderInfo extends BaseRenderInfo { if (prev && Types.isLeftSquareCorner(prev) && next && Types.isHat(next)) { return this.constants_.NO_PADDING; } + + // No space after zero-width fields. + if (prev && Types.isField(prev) && prev.width === 0) { + return this.constants_.NO_PADDING; + } + return this.constants_.MEDIUM_PADDING; } @@ -207,9 +212,8 @@ export class RenderInfo extends BaseRenderInfo { } // Top and bottom rows act as a spacer so we don't need any extra padding. if (Types.isTopRow(prev)) { - const topRow = prev as TopRow; if ( - !topRow.hasPreviousConnection && + !prev.hasPreviousConnection && (!this.outputConnection || this.hasStatementInput) ) { return Math.abs( @@ -219,7 +223,6 @@ export class RenderInfo extends BaseRenderInfo { return this.constants_.NO_PADDING; } if (Types.isBottomRow(next)) { - const bottomRow = next as BottomRow; if (!this.outputConnection) { const topHeight = Math.max( @@ -230,7 +233,7 @@ export class RenderInfo extends BaseRenderInfo { ), ) - this.constants_.CORNER_RADIUS; return topHeight; - } else if (!bottomRow.hasNextConnection && this.hasStatementInput) { + } else if (!next.hasNextConnection && this.hasStatementInput) { return Math.abs( this.constants_.NOTCH_HEIGHT - this.constants_.CORNER_RADIUS, ); @@ -259,7 +262,7 @@ export class RenderInfo extends BaseRenderInfo { ) { return row.yPos + this.constants_.EMPTY_STATEMENT_INPUT_HEIGHT / 2; } - if (Types.isInlineInput(elem) && elem instanceof InputConnection) { + if (Types.isInlineInput(elem)) { const connectedBlock = elem.connectedBlock; if ( connectedBlock && @@ -308,7 +311,6 @@ export class RenderInfo extends BaseRenderInfo { } if ( Types.isField(elem) && - elem instanceof Field && elem.parentInput === this.rightAlignedDummyInputs.get(row) ) { break; @@ -371,7 +373,6 @@ export class RenderInfo extends BaseRenderInfo { xCursor < minXPos && !( Types.isField(elem) && - elem instanceof Field && (elem.field instanceof FieldLabel || elem.field instanceof FieldImage) ) @@ -525,7 +526,7 @@ export class RenderInfo extends BaseRenderInfo { return 0; } } - if (Types.isInlineInput(elem) && elem instanceof InputConnection) { + if (Types.isInlineInput(elem)) { const connectedBlock = elem.connectedBlock; const innerShape = connectedBlock ? (connectedBlock.pathObject as PathObject).outputShapeType @@ -552,7 +553,7 @@ export class RenderInfo extends BaseRenderInfo { connectionWidth - this.constants_.SHAPE_IN_SHAPE_PADDING[outerShape][innerShape] ); - } else if (Types.isField(elem) && elem instanceof Field) { + } else if (Types.isField(elem)) { // Special case for text inputs. if ( outerShape === constants.SHAPES.ROUND && @@ -616,7 +617,6 @@ export class RenderInfo extends BaseRenderInfo { for (let j = 0; j < row.elements.length; j++) { const elem = row.elements[j]; if ( - elem instanceof InputConnection && Types.isInlineInput(elem) && elem.connectedBlock && !elem.connectedBlock.isShadow() && diff --git a/core/renderers/zelos/measurables/bottom_row.ts b/packages/blockly/core/renderers/zelos/measurables/bottom_row.ts similarity index 100% rename from core/renderers/zelos/measurables/bottom_row.ts rename to packages/blockly/core/renderers/zelos/measurables/bottom_row.ts diff --git a/core/renderers/zelos/measurables/inputs.ts b/packages/blockly/core/renderers/zelos/measurables/inputs.ts similarity index 100% rename from core/renderers/zelos/measurables/inputs.ts rename to packages/blockly/core/renderers/zelos/measurables/inputs.ts diff --git a/core/renderers/zelos/measurables/row_elements.ts b/packages/blockly/core/renderers/zelos/measurables/row_elements.ts similarity index 100% rename from core/renderers/zelos/measurables/row_elements.ts rename to packages/blockly/core/renderers/zelos/measurables/row_elements.ts diff --git a/core/renderers/zelos/measurables/top_row.ts b/packages/blockly/core/renderers/zelos/measurables/top_row.ts similarity index 100% rename from core/renderers/zelos/measurables/top_row.ts rename to packages/blockly/core/renderers/zelos/measurables/top_row.ts diff --git a/core/renderers/zelos/path_object.ts b/packages/blockly/core/renderers/zelos/path_object.ts similarity index 89% rename from core/renderers/zelos/path_object.ts rename to packages/blockly/core/renderers/zelos/path_object.ts index a46d355b674..3c304fd6bf8 100644 --- a/core/renderers/zelos/path_object.ts +++ b/packages/blockly/core/renderers/zelos/path_object.ts @@ -8,6 +8,7 @@ import type {BlockSvg} from '../../block_svg.js'; import type {Connection} from '../../connection.js'; +import {FocusManager} from '../../focus_manager.js'; import type {BlockStyle} from '../../theme.js'; import * as dom from '../../utils/dom.js'; import {Svg} from '../../utils/svg.js'; @@ -90,11 +91,18 @@ export class PathObject extends BasePathObject { if (enable) { if (!this.svgPathSelected) { this.svgPathSelected = this.svgPath.cloneNode(true) as SVGElement; - this.svgPathSelected.setAttribute('fill', 'none'); - this.svgPathSelected.setAttribute( - 'filter', - 'url(#' + this.constants.selectedGlowFilterId + ')', + this.svgPathSelected.classList.add('blocklyPathSelected'); + // Ensure focus-specific properties don't overlap with the block's path. + dom.removeClass( + this.svgPathSelected, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, ); + dom.removeClass( + this.svgPathSelected, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + this.svgPathSelected.removeAttribute('tabindex'); + this.svgPathSelected.removeAttribute('id'); this.svgRoot.appendChild(this.svgPathSelected); } } else { @@ -107,14 +115,6 @@ export class PathObject extends BasePathObject { override updateReplacementFade(enable: boolean) { this.setClass_('blocklyReplaceable', enable); - if (enable) { - this.svgPath.setAttribute( - 'filter', - 'url(#' + this.constants.replacementGlowFilterId + ')', - ); - } else { - this.svgPath.removeAttribute('filter'); - } } override updateShapeForInputHighlight(conn: Connection, enable: boolean) { @@ -173,10 +173,11 @@ export class PathObject extends BasePathObject { /** * Create's an outline path for the specified input. * + * @internal * @param name The input name. * @returns The SVG outline path. */ - private getOutlinePath(name: string): SVGElement { + getOutlinePath(name: string): SVGElement { if (!this.outlines.has(name)) { this.outlines.set( name, diff --git a/core/renderers/zelos/renderer.ts b/packages/blockly/core/renderers/zelos/renderer.ts similarity index 57% rename from core/renderers/zelos/renderer.ts rename to packages/blockly/core/renderers/zelos/renderer.ts index b48600a0b4d..367d96faf51 100644 --- a/core/renderers/zelos/renderer.ts +++ b/packages/blockly/core/renderers/zelos/renderer.ts @@ -7,20 +7,13 @@ // Former goog.module ID: Blockly.zelos.Renderer import type {BlockSvg} from '../../block_svg.js'; -import {ConnectionType} from '../../connection_type.js'; -import {InsertionMarkerManager} from '../../insertion_marker_manager.js'; -import type {Marker} from '../../keyboard_nav/marker.js'; -import type {RenderedConnection} from '../../rendered_connection.js'; import type {BlockStyle} from '../../theme.js'; -import * as deprecation from '../../utils/deprecation.js'; -import type {WorkspaceSvg} from '../../workspace_svg.js'; import * as blockRendering from '../common/block_rendering.js'; import type {RenderInfo as BaseRenderInfo} from '../common/info.js'; import {Renderer as BaseRenderer} from '../common/renderer.js'; import {ConstantProvider} from './constants.js'; import {Drawer} from './drawer.js'; import {RenderInfo} from './info.js'; -import {MarkerSvg} from './marker_svg.js'; import {PathObject} from './path_object.js'; /** @@ -73,20 +66,6 @@ export class Renderer extends BaseRenderer { return new Drawer(block, info as RenderInfo); } - /** - * Create a new instance of the renderer's cursor drawer. - * - * @param workspace The workspace the cursor belongs to. - * @param marker The marker. - * @returns The object in charge of drawing the marker. - */ - override makeMarkerDrawer( - workspace: WorkspaceSvg, - marker: Marker, - ): MarkerSvg { - return new MarkerSvg(workspace, this.getConstants(), marker); - } - /** * Create a new instance of a renderer path object. * @@ -107,36 +86,6 @@ export class Renderer extends BaseRenderer { override getConstants(): ConstantProvider { return this.constants_; } - - /** - * @deprecated v10 - This function is no longer respected. A custom - * IConnectionPreviewer may be able to fulfill the functionality. - */ - override getConnectionPreviewMethod( - closest: RenderedConnection, - local: RenderedConnection, - topBlock: BlockSvg, - ) { - deprecation.warn( - 'getConnectionPreviewMethod', - 'v10', - 'v12', - 'an IConnectionPreviewer, if it fulfills your use case.', - ); - if (local.type === ConnectionType.OUTPUT_VALUE) { - if (!closest.isConnected()) { - return InsertionMarkerManager.PREVIEW_TYPE.INPUT_OUTLINE; - } - // TODO: Returning this is a total hack, because we don't want to show - // a replacement fade, we want to show an outline affect. - // Sadly zelos does not support showing an outline around filled - // inputs, so we have to pretend like the connected block is getting - // replaced. - return InsertionMarkerManager.PREVIEW_TYPE.REPLACEMENT_FADE; - } - - return super.getConnectionPreviewMethod(closest, local, topBlock); - } } blockRendering.register('zelos', Renderer); diff --git a/core/renderers/zelos/zelos.ts b/packages/blockly/core/renderers/zelos/zelos.ts similarity index 93% rename from core/renderers/zelos/zelos.ts rename to packages/blockly/core/renderers/zelos/zelos.ts index c28a0210c6d..5b0a7c51c60 100644 --- a/core/renderers/zelos/zelos.ts +++ b/packages/blockly/core/renderers/zelos/zelos.ts @@ -11,7 +11,6 @@ import {ConstantProvider} from './constants.js'; import {Drawer} from './drawer.js'; import {RenderInfo} from './info.js'; -import {MarkerSvg} from './marker_svg.js'; import {BottomRow} from './measurables/bottom_row.js'; import {StatementInput} from './measurables/inputs.js'; import {RightConnectionShape} from './measurables/row_elements.js'; @@ -23,7 +22,6 @@ export { BottomRow, ConstantProvider, Drawer, - MarkerSvg, PathObject, Renderer, RenderInfo, diff --git a/core/scrollbar.ts b/packages/blockly/core/scrollbar.ts similarity index 100% rename from core/scrollbar.ts rename to packages/blockly/core/scrollbar.ts diff --git a/core/scrollbar_pair.ts b/packages/blockly/core/scrollbar_pair.ts similarity index 100% rename from core/scrollbar_pair.ts rename to packages/blockly/core/scrollbar_pair.ts diff --git a/packages/blockly/core/separator_flyout_inflater.ts b/packages/blockly/core/separator_flyout_inflater.ts new file mode 100644 index 00000000000..0c7897b0f03 --- /dev/null +++ b/packages/blockly/core/separator_flyout_inflater.ts @@ -0,0 +1,83 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {FlyoutItem} from './flyout_item.js'; +import {FlyoutSeparator, SeparatorAxis} from './flyout_separator.js'; +import type {IFlyout} from './interfaces/i_flyout.js'; +import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js'; +import * as registry from './registry.js'; +import type {SeparatorInfo} from './utils/toolbox.js'; + +/** + * @internal + */ +export const SEPARATOR_TYPE = 'sep'; + +/** + * Class responsible for creating separators for flyouts. + */ +export class SeparatorFlyoutInflater implements IFlyoutInflater { + /** + * Inflates a dummy flyout separator. + * + * The flyout automatically creates separators between every element with a + * size determined by calling gapForElement on the relevant inflater. + * Additionally, users can explicitly add separators in the flyout definition. + * When separators (implicitly or explicitly created) follow one another, the + * gap of the last one propagates backwards and flattens to one separator. + * This flattening is not additive; if there are initially separators of 2, 3, + * and 4 pixels, after normalization there will be one separator of 4 pixels. + * Therefore, this method returns a zero-width separator, which will be + * replaced by the one implicitly created by the flyout based on the value + * returned by gapForElement, which knows the default gap, unlike this method. + * + * @param _state A JSON representation of a flyout separator. + * @param flyout The flyout to create the separator for. + * @returns A newly created FlyoutSeparator. + */ + load(_state: object, flyout: IFlyout): FlyoutItem { + const flyoutAxis = flyout.horizontalLayout + ? SeparatorAxis.X + : SeparatorAxis.Y; + const separator = new FlyoutSeparator(0, flyoutAxis); + return new FlyoutItem(separator, SEPARATOR_TYPE); + } + + /** + * Returns the size of the separator. See `load` for more details. + * + * @param state A JSON representation of a flyout separator. + * @param defaultGap The default spacing for flyout items. + * @returns The desired size of the separator. + */ + gapForItem(state: object, defaultGap: number): number { + const separatorState = state as SeparatorInfo; + const newGap = parseInt(String(separatorState['gap'])); + return newGap ?? defaultGap; + } + + /** + * Disposes of the given separator. Intentional no-op. + * + * @param _item The flyout separator to dispose of. + */ + disposeItem(_item: FlyoutItem): void {} + + /** + * Returns the type of items this inflater is responsible for creating. + * + * @returns An identifier for the type of items this inflater creates. + */ + getType() { + return SEPARATOR_TYPE; + } +} + +registry.register( + registry.Type.FLYOUT_INFLATER, + SEPARATOR_TYPE, + SeparatorFlyoutInflater, +); diff --git a/core/serialization.ts b/packages/blockly/core/serialization.ts similarity index 100% rename from core/serialization.ts rename to packages/blockly/core/serialization.ts diff --git a/core/serialization/blocks.ts b/packages/blockly/core/serialization/blocks.ts similarity index 91% rename from core/serialization/blocks.ts rename to packages/blockly/core/serialization/blocks.ts index b9026224063..af8910b3137 100644 --- a/core/serialization/blocks.ts +++ b/packages/blockly/core/serialization/blocks.ts @@ -15,10 +15,13 @@ import * as eventUtils from '../events/utils.js'; import {inputTypes} from '../inputs/input_types.js'; import {isSerializable} from '../interfaces/i_serializable.js'; import type {ISerializer} from '../interfaces/i_serializer.js'; +import type { + IVariableModel, + IVariableState, +} from '../interfaces/i_variable_model.js'; import * as registry from '../registry.js'; import * as renderManagement from '../render_management.js'; import * as utilsXml from '../utils/xml.js'; -import {VariableModel} from '../variable_model.js'; import * as Variables from '../variables.js'; import type {Workspace} from '../workspace.js'; import * as Xml from '../xml.js'; @@ -32,6 +35,8 @@ import { import * as priorities from './priorities.js'; import * as serializationRegistry from './registry.js'; +// TODO(#5160): Remove this once lint is fixed. + /** * Represents the state of a connection. */ @@ -99,52 +104,26 @@ export function save( if (block.isInsertionMarker()) { return null; } - const state = { + const state: State = { 'type': block.type, 'id': saveIds ? block.id : undefined, }; if (addCoordinates) { - // AnyDuringMigration because: Argument of type '{ type: string; id: - // string; }' is not assignable to parameter of type 'State'. - saveCoords(block, state as AnyDuringMigration); - } - // AnyDuringMigration because: Argument of type '{ type: string; id: string; - // }' is not assignable to parameter of type 'State'. - saveAttributes(block, state as AnyDuringMigration); - // AnyDuringMigration because: Argument of type '{ type: string; id: string; - // }' is not assignable to parameter of type 'State'. - saveExtraState(block, state as AnyDuringMigration, doFullSerialization); - // AnyDuringMigration because: Argument of type '{ type: string; id: string; - // }' is not assignable to parameter of type 'State'. - saveIcons(block, state as AnyDuringMigration, doFullSerialization); - // AnyDuringMigration because: Argument of type '{ type: string; id: string; - // }' is not assignable to parameter of type 'State'. - saveFields(block, state as AnyDuringMigration, doFullSerialization); + saveCoords(block, state); + } + saveAttributes(block, state); + saveExtraState(block, state, doFullSerialization); + saveIcons(block, state, doFullSerialization); + saveFields(block, state, doFullSerialization); if (addInputBlocks) { - // AnyDuringMigration because: Argument of type '{ type: string; id: - // string; }' is not assignable to parameter of type 'State'. - saveInputBlocks( - block, - state as AnyDuringMigration, - doFullSerialization, - saveIds, - ); + saveInputBlocks(block, state, doFullSerialization, saveIds); } if (addNextBlocks) { - // AnyDuringMigration because: Argument of type '{ type: string; id: - // string; }' is not assignable to parameter of type 'State'. - saveNextBlocks( - block, - state as AnyDuringMigration, - doFullSerialization, - saveIds, - ); + saveNextBlocks(block, state, doFullSerialization, saveIds); } - // AnyDuringMigration because: Type '{ type: string; id: string; }' is not - // assignable to type 'State'. - return state as AnyDuringMigration; + return state; } /** @@ -256,13 +235,9 @@ function saveIcons(block: Block, state: State, doFullSerialization: boolean) { */ function saveFields(block: Block, state: State, doFullSerialization: boolean) { const fields = Object.create(null); - for (let i = 0; i < block.inputList.length; i++) { - const input = block.inputList[i]; - for (let j = 0; j < input.fieldRow.length; j++) { - const field = input.fieldRow[j]; - if (field.isSerializable()) { - fields[field.name!] = field.saveState(doFullSerialization); - } + for (const field of block.getFields()) { + if (field.isSerializable()) { + fields[field.name!] = field.saveState(doFullSerialization); } } if (Object.keys(fields).length) { @@ -416,7 +391,7 @@ export function appendInternal( } eventUtils.disable(); - const variablesBeforeCreation = workspace.getAllVariables(); + const variablesBeforeCreation = workspace.getVariableMap().getAllVariables(); let block; try { block = appendPrivate(state, workspace, {parentConnection, isShadow}); @@ -500,7 +475,7 @@ function appendPrivate( */ function checkNewVariables( workspace: Workspace, - originalVariables: VariableModel[], + originalVariables: IVariableModel[], ) { if (eventUtils.isEnabled()) { const newVariables = Variables.getAddedVariables( diff --git a/core/serialization/exceptions.ts b/packages/blockly/core/serialization/exceptions.ts similarity index 100% rename from core/serialization/exceptions.ts rename to packages/blockly/core/serialization/exceptions.ts diff --git a/core/serialization/priorities.ts b/packages/blockly/core/serialization/priorities.ts similarity index 100% rename from core/serialization/priorities.ts rename to packages/blockly/core/serialization/priorities.ts diff --git a/core/serialization/procedures.ts b/packages/blockly/core/serialization/procedures.ts similarity index 100% rename from core/serialization/procedures.ts rename to packages/blockly/core/serialization/procedures.ts diff --git a/core/serialization/registry.ts b/packages/blockly/core/serialization/registry.ts similarity index 100% rename from core/serialization/registry.ts rename to packages/blockly/core/serialization/registry.ts diff --git a/core/serialization/variables.ts b/packages/blockly/core/serialization/variables.ts similarity index 59% rename from core/serialization/variables.ts rename to packages/blockly/core/serialization/variables.ts index e4fc7fbaab8..a896606fa4e 100644 --- a/core/serialization/variables.ts +++ b/packages/blockly/core/serialization/variables.ts @@ -7,19 +7,12 @@ // Former goog.module ID: Blockly.serialization.variables import type {ISerializer} from '../interfaces/i_serializer.js'; +import type {IVariableState} from '../interfaces/i_variable_model.js'; +import * as registry from '../registry.js'; import type {Workspace} from '../workspace.js'; import * as priorities from './priorities.js'; import * as serializationRegistry from './registry.js'; -/** - * Represents the state of a given variable. - */ -export interface State { - name: string; - id: string; - type: string | undefined; -} - /** * Serializer for saving and loading variable state. */ @@ -38,23 +31,12 @@ export class VariableSerializer implements ISerializer { * @returns The state of the workspace's variables, or null if there are no * variables. */ - save(workspace: Workspace): State[] | null { - const variableStates = []; - for (const variable of workspace.getAllVariables()) { - const state = { - 'name': variable.name, - 'id': variable.getId(), - }; - if (variable.type) { - (state as AnyDuringMigration)['type'] = variable.type; - } - variableStates.push(state); - } - // AnyDuringMigration because: Type '{ name: string; id: string; }[] | - // null' is not assignable to type 'State[] | null'. - return ( - variableStates.length ? variableStates : null - ) as AnyDuringMigration; + save(workspace: Workspace): IVariableState[] | null { + const variableStates = workspace + .getVariableMap() + .getAllVariables() + .map((v) => v.save()); + return variableStates.length ? variableStates : null; } /** @@ -64,14 +46,14 @@ export class VariableSerializer implements ISerializer { * @param state The state of the variables to deserialize. * @param workspace The workspace to deserialize into. */ - load(state: State[], workspace: Workspace) { - for (const varState of state) { - workspace.createVariable( - varState['name'], - varState['type'], - varState['id'], - ); - } + load(state: IVariableState[], workspace: Workspace) { + const VariableModel = registry.getObject( + registry.Type.VARIABLE_MODEL, + registry.DEFAULT, + ); + state.forEach((s) => { + VariableModel?.load(s, workspace); + }); } /** diff --git a/core/serialization/workspace_comments.ts b/packages/blockly/core/serialization/workspace_comments.ts similarity index 98% rename from core/serialization/workspace_comments.ts rename to packages/blockly/core/serialization/workspace_comments.ts index 61d1127b39c..2c40a892ad5 100644 --- a/core/serialization/workspace_comments.ts +++ b/packages/blockly/core/serialization/workspace_comments.ts @@ -110,7 +110,7 @@ export class WorkspaceCommentSerializer implements ISerializer { save(workspace: Workspace): State[] | null { const commentStates = []; for (const comment of workspace.getTopComments()) { - const state = saveComment(comment as AnyDuringMigration, { + const state = saveComment(comment, { addCoordinates: true, saveIds: true, }); diff --git a/core/serialization/workspaces.ts b/packages/blockly/core/serialization/workspaces.ts similarity index 100% rename from core/serialization/workspaces.ts rename to packages/blockly/core/serialization/workspaces.ts diff --git a/packages/blockly/core/shortcut_items.ts b/packages/blockly/core/shortcut_items.ts new file mode 100644 index 00000000000..f8c95500770 --- /dev/null +++ b/packages/blockly/core/shortcut_items.ts @@ -0,0 +1,405 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Former goog.module ID: Blockly.ShortcutItems + +import {BlockSvg} from './block_svg.js'; +import * as clipboard from './clipboard.js'; +import {RenderedWorkspaceComment} from './comments.js'; +import * as eventUtils from './events/utils.js'; +import {getFocusManager} from './focus_manager.js'; +import {isCopyable as isICopyable} from './interfaces/i_copyable.js'; +import {isDeletable as isIDeletable} from './interfaces/i_deletable.js'; +import {isDraggable} from './interfaces/i_draggable.js'; +import {IFocusableNode} from './interfaces/i_focusable_node.js'; +import {KeyboardShortcut, ShortcutRegistry} from './shortcut_registry.js'; +import {Coordinate} from './utils/coordinate.js'; +import {KeyCodes} from './utils/keycodes.js'; +import {Rect} from './utils/rect.js'; +import * as svgMath from './utils/svg_math.js'; +import {WorkspaceSvg} from './workspace_svg.js'; + +/** + * Object holding the names of the default shortcut items. + */ +export enum names { + ESCAPE = 'escape', + DELETE = 'delete', + COPY = 'copy', + CUT = 'cut', + PASTE = 'paste', + UNDO = 'undo', + REDO = 'redo', +} + +/** + * Keyboard shortcut to hide chaff on escape. + */ +export function registerEscape() { + const escapeAction: KeyboardShortcut = { + name: names.ESCAPE, + preconditionFn(workspace) { + return !workspace.isReadOnly(); + }, + callback(workspace) { + workspace.hideChaff(); + return true; + }, + keyCodes: [KeyCodes.ESC], + }; + ShortcutRegistry.registry.register(escapeAction); +} + +/** + * Keyboard shortcut to delete a block on delete or backspace + */ +export function registerDelete() { + const deleteShortcut: KeyboardShortcut = { + name: names.DELETE, + preconditionFn(workspace, scope) { + const focused = scope.focusedNode; + return ( + !workspace.isReadOnly() && + focused != null && + isIDeletable(focused) && + focused.isDeletable() && + !workspace.isDragging() && + // Don't delete the block if a field editor is open + !getFocusManager().ephemeralFocusTaken() + ); + }, + callback(workspace, e, shortcut, scope) { + // 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(); + const focused = scope.focusedNode; + if (focused instanceof BlockSvg) { + focused.checkAndDelete(); + } else if (isIDeletable(focused) && focused.isDeletable()) { + eventUtils.setGroup(true); + focused.dispose(); + eventUtils.setGroup(false); + } + return true; + }, + keyCodes: [KeyCodes.DELETE, KeyCodes.BACKSPACE], + }; + ShortcutRegistry.registry.register(deleteShortcut); +} + +/** + * Determine if a focusable node can be copied. + * + * This will use the isCopyable method if the node implements it, otherwise + * it will fall back to checking if the node is deletable and draggable not + * considering the workspace's edit state. + * + * @param focused The focused object. + */ +function isCopyable(focused: IFocusableNode): boolean { + if (!isICopyable(focused) || !isIDeletable(focused) || !isDraggable(focused)) + return false; + if (focused.isCopyable) { + return focused.isCopyable(); + } else if ( + focused instanceof BlockSvg || + focused instanceof RenderedWorkspaceComment + ) { + return focused.isOwnDeletable() && focused.isOwnMovable(); + } + // This isn't a class Blockly knows about, so fall back to the stricter + // checks for deletable and movable. + return focused.isDeletable() && focused.isMovable(); +} + +/** + * Determine if a focusable node can be cut. + * + * This will check if the node can be both copied and deleted in its current + * workspace. + * + * @param focused The focused object. + */ +function isCuttable(focused: IFocusableNode): boolean { + return isCopyable(focused) && isIDeletable(focused) && focused.isDeletable(); +} + +/** + * Keyboard shortcut to copy a block on ctrl+c, cmd+c, or alt+c. + */ +export function registerCopy() { + const ctrlC = ShortcutRegistry.registry.createSerializedKey(KeyCodes.C, [ + KeyCodes.CTRL, + ]); + const metaC = ShortcutRegistry.registry.createSerializedKey(KeyCodes.C, [ + KeyCodes.META, + ]); + + const copyShortcut: KeyboardShortcut = { + name: names.COPY, + preconditionFn(workspace, scope) { + const focused = scope.focusedNode; + + const targetWorkspace = workspace.isFlyout + ? workspace.targetWorkspace + : workspace; + return ( + !!focused && + !!targetWorkspace && + !targetWorkspace.isDragging() && + !getFocusManager().ephemeralFocusTaken() && + isCopyable(focused) + ); + }, + callback(workspace, e, shortcut, scope) { + // Prevent the default copy behavior, which may beep or otherwise indicate + // an error due to the lack of a selection. + e.preventDefault(); + + const focused = scope.focusedNode; + if (!focused || !isICopyable(focused) || !isCopyable(focused)) + return false; + const targetWorkspace = workspace.isFlyout + ? workspace.targetWorkspace + : workspace; + if (!targetWorkspace) return false; + + if (!focused.workspace.isFlyout) { + targetWorkspace.hideChaff(); + } + + const copyCoords = + isDraggable(focused) && focused.workspace == targetWorkspace + ? focused.getRelativeToSurfaceXY() + : undefined; + return !!clipboard.copy(focused, copyCoords); + }, + keyCodes: [ctrlC, metaC], + }; + ShortcutRegistry.registry.register(copyShortcut); +} + +/** + * Keyboard shortcut to copy and delete a block on ctrl+x, cmd+x, or alt+x. + */ +export function registerCut() { + const ctrlX = ShortcutRegistry.registry.createSerializedKey(KeyCodes.X, [ + KeyCodes.CTRL, + ]); + const metaX = ShortcutRegistry.registry.createSerializedKey(KeyCodes.X, [ + KeyCodes.META, + ]); + + const cutShortcut: KeyboardShortcut = { + name: names.CUT, + preconditionFn(workspace, scope) { + const focused = scope.focusedNode; + return ( + !!focused && + !workspace.isReadOnly() && + !workspace.isDragging() && + !getFocusManager().ephemeralFocusTaken() && + isCuttable(focused) + ); + }, + callback(workspace, e, shortcut, scope) { + const focused = scope.focusedNode; + if (!focused || !isCuttable(focused) || !isICopyable(focused)) { + return false; + } + const copyCoords = isDraggable(focused) + ? focused.getRelativeToSurfaceXY() + : undefined; + const copyData = clipboard.copy(focused, copyCoords); + + if (focused instanceof BlockSvg) { + focused.checkAndDelete(); + } else if (isIDeletable(focused)) { + focused.dispose(); + } + return !!copyData; + }, + keyCodes: [ctrlX, metaX], + }; + + ShortcutRegistry.registry.register(cutShortcut); +} + +/** + * Keyboard shortcut to paste a block on ctrl+v, cmd+v, or alt+v. + */ +export function registerPaste() { + const ctrlV = ShortcutRegistry.registry.createSerializedKey(KeyCodes.V, [ + KeyCodes.CTRL, + ]); + const metaV = ShortcutRegistry.registry.createSerializedKey(KeyCodes.V, [ + KeyCodes.META, + ]); + + const pasteShortcut: KeyboardShortcut = { + name: names.PASTE, + preconditionFn() { + // Regardless of the currently focused workspace, we will only + // paste into the last-copied-from workspace. + const workspace = clipboard.getLastCopiedWorkspace(); + // If we don't know where we copied from, we don't know where to paste. + // If the workspace isn't rendered (e.g. closed mutator workspace), + // we can't paste into it. + if (!workspace || !workspace.rendered) return false; + const targetWorkspace = workspace.isFlyout + ? workspace.targetWorkspace + : workspace; + return ( + !!clipboard.getLastCopiedData() && + !!targetWorkspace && + !targetWorkspace.isReadOnly() && + !targetWorkspace.isDragging() && + !getFocusManager().ephemeralFocusTaken() + ); + }, + callback(workspace: WorkspaceSvg, e: Event) { + const copyData = clipboard.getLastCopiedData(); + if (!copyData) return false; + + const copyWorkspace = clipboard.getLastCopiedWorkspace(); + if (!copyWorkspace) return false; + + const targetWorkspace = copyWorkspace.isFlyout + ? copyWorkspace.targetWorkspace + : copyWorkspace; + if (!targetWorkspace || targetWorkspace.isReadOnly()) return false; + + if (e instanceof PointerEvent) { + // The event that triggers a shortcut would conventionally be a KeyboardEvent. + // However, it may be a PointerEvent if a context menu item was used as a + // wrapper for this callback, in which case the new block(s) should be pasted + // at the mouse coordinates where the menu was opened, and this PointerEvent + // is where the menu was opened. + const mouseCoords = svgMath.screenToWsCoordinates( + targetWorkspace, + new Coordinate(e.clientX, e.clientY), + ); + return !!clipboard.paste(copyData, targetWorkspace, mouseCoords); + } + + const copyCoords = clipboard.getLastCopiedLocation(); + if (!copyCoords) { + // If we don't have location data about the original copyable, let the + // paster determine position. + return !!clipboard.paste(copyData, targetWorkspace); + } + + const {left, top, width, height} = targetWorkspace + .getMetricsManager() + .getViewMetrics(true); + const viewportRect = new Rect(top, top + height, left, left + width); + + if (viewportRect.contains(copyCoords.x, copyCoords.y)) { + // If the original copyable is inside the viewport, let the paster + // determine position. + return !!clipboard.paste(copyData, targetWorkspace); + } + + // Otherwise, paste in the middle of the viewport. + const centerCoords = new Coordinate(left + width / 2, top + height / 2); + return !!clipboard.paste(copyData, targetWorkspace, centerCoords); + }, + keyCodes: [ctrlV, metaV], + }; + + ShortcutRegistry.registry.register(pasteShortcut); +} + +/** + * Keyboard shortcut to undo the previous action on ctrl+z, cmd+z, or alt+z. + */ +export function registerUndo() { + const ctrlZ = ShortcutRegistry.registry.createSerializedKey(KeyCodes.Z, [ + KeyCodes.CTRL, + ]); + const metaZ = ShortcutRegistry.registry.createSerializedKey(KeyCodes.Z, [ + KeyCodes.META, + ]); + + const undoShortcut: KeyboardShortcut = { + name: names.UNDO, + preconditionFn(workspace) { + return ( + !workspace.isReadOnly() && + !workspace.isDragging() && + !getFocusManager().ephemeralFocusTaken() + ); + }, + callback(workspace, e) { + // 'z' for undo 'Z' is for redo. + (workspace as WorkspaceSvg).hideChaff(); + workspace.undo(false); + e.preventDefault(); + return true; + }, + keyCodes: [ctrlZ, metaZ], + }; + ShortcutRegistry.registry.register(undoShortcut); +} + +/** + * Keyboard shortcut to redo the previous action on ctrl+shift+z, cmd+shift+z, + * or alt+shift+z. + */ +export function registerRedo() { + const ctrlShiftZ = ShortcutRegistry.registry.createSerializedKey(KeyCodes.Z, [ + KeyCodes.CTRL, + KeyCodes.SHIFT, + ]); + const metaShiftZ = ShortcutRegistry.registry.createSerializedKey(KeyCodes.Z, [ + KeyCodes.META, + KeyCodes.SHIFT, + ]); + // Ctrl-y is redo in Windows. Command-y is never valid on Macs. + const ctrlY = ShortcutRegistry.registry.createSerializedKey(KeyCodes.Y, [ + KeyCodes.CTRL, + ]); + + const redoShortcut: KeyboardShortcut = { + name: names.REDO, + preconditionFn(workspace) { + return ( + !workspace.isDragging() && + !workspace.isReadOnly() && + !getFocusManager().ephemeralFocusTaken() + ); + }, + callback(workspace, e) { + // 'z' for undo 'Z' is for redo. + (workspace as WorkspaceSvg).hideChaff(); + workspace.undo(true); + e.preventDefault(); + return true; + }, + keyCodes: [ctrlShiftZ, metaShiftZ, ctrlY], + }; + ShortcutRegistry.registry.register(redoShortcut); +} + +/** + * Registers all default keyboard shortcut item. This should be called once per + * instance of KeyboardShortcutRegistry. + * + * @internal + */ +export function registerDefaultShortcuts() { + registerEscape(); + registerDelete(); + registerCopy(); + registerCut(); + registerPaste(); + registerUndo(); + registerRedo(); +} + +registerDefaultShortcuts(); diff --git a/core/shortcut_registry.ts b/packages/blockly/core/shortcut_registry.ts similarity index 93% rename from core/shortcut_registry.ts rename to packages/blockly/core/shortcut_registry.ts index 09bd867e769..f40149db816 100644 --- a/core/shortcut_registry.ts +++ b/packages/blockly/core/shortcut_registry.ts @@ -12,6 +12,8 @@ */ // Former goog.module ID: Blockly.ShortcutRegistry +import {Scope} from './contextmenu_registry.js'; +import {getFocusManager} from './focus_manager.js'; import {KeyCodes} from './utils/keycodes.js'; import * as object from './utils/object.js'; import {WorkspaceSvg} from './workspace_svg.js'; @@ -249,12 +251,21 @@ export class ShortcutRegistry { const shortcut = this.shortcuts.get(shortcutName); if ( !shortcut || - (shortcut.preconditionFn && !shortcut.preconditionFn(workspace)) + (shortcut.preconditionFn && + !shortcut.preconditionFn(workspace, { + focusedNode: getFocusManager().getFocusedNode() ?? undefined, + })) ) { continue; } // If the key has been handled, stop processing shortcuts. - if (shortcut.callback?.(workspace, e, shortcut)) return true; + if ( + shortcut.callback?.(workspace, e, shortcut, { + focusedNode: getFocusManager().getFocusedNode() ?? undefined, + }) + ) { + return true; + } } return false; } @@ -267,7 +278,9 @@ export class ShortcutRegistry { * Undefined if no shortcuts exist. */ getShortcutNamesByKeyCode(keyCode: string): string[] | undefined { - return this.keyMap.get(keyCode) || []; + // Copy the list of shortcuts in case one of them unregisters itself + // in its callback. + return this.keyMap.get(keyCode)?.slice() || []; } /** @@ -372,6 +385,8 @@ export namespace ShortcutRegistry { * @param e The event that caused the shortcut to be activated. * @param shortcut The `KeyboardShortcut` that was activated * (i.e., the one this callback is attached to). + * @param scope Information about the focused item when the + * shortcut was invoked. * @returns Returning true ends processing of the invoked keycode. * Returning false causes processing to continue with the * next-most-recently registered shortcut for the invoked @@ -381,6 +396,7 @@ export namespace ShortcutRegistry { workspace: WorkspaceSvg, e: Event, shortcut: KeyboardShortcut, + scope: Scope, ) => boolean; /** The name of the shortcut. Should be unique. */ @@ -393,9 +409,11 @@ export namespace ShortcutRegistry { * * @param workspace The `WorkspaceSvg` where the shortcut was * invoked. + * @param scope Information about the focused item when the + * shortcut would be invoked. * @returns True iff `callback` function should be called. */ - preconditionFn?: (workspace: WorkspaceSvg) => boolean; + preconditionFn?: (workspace: WorkspaceSvg, scope: Scope) => boolean; /** Optional arbitray extra data attached to the shortcut. */ metadata?: object; diff --git a/core/sprites.ts b/packages/blockly/core/sprites.ts similarity index 100% rename from core/sprites.ts rename to packages/blockly/core/sprites.ts diff --git a/core/theme.ts b/packages/blockly/core/theme.ts similarity index 100% rename from core/theme.ts rename to packages/blockly/core/theme.ts diff --git a/core/theme/classic.ts b/packages/blockly/core/theme/classic.ts similarity index 100% rename from core/theme/classic.ts rename to packages/blockly/core/theme/classic.ts diff --git a/core/theme/themes.ts b/packages/blockly/core/theme/themes.ts similarity index 100% rename from core/theme/themes.ts rename to packages/blockly/core/theme/themes.ts diff --git a/core/theme/zelos.ts b/packages/blockly/core/theme/zelos.ts similarity index 100% rename from core/theme/zelos.ts rename to packages/blockly/core/theme/zelos.ts diff --git a/core/theme_manager.ts b/packages/blockly/core/theme_manager.ts similarity index 100% rename from core/theme_manager.ts rename to packages/blockly/core/theme_manager.ts diff --git a/packages/blockly/core/toast.ts b/packages/blockly/core/toast.ts new file mode 100644 index 00000000000..72559279f57 --- /dev/null +++ b/packages/blockly/core/toast.ts @@ -0,0 +1,219 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as Css from './css.js'; +import {Msg} from './msg.js'; +import * as aria from './utils/aria.js'; +import * as dom from './utils/dom.js'; +import {Svg} from './utils/svg.js'; +import type {WorkspaceSvg} from './workspace_svg.js'; + +const CLASS_NAME = 'blocklyToast'; +const MESSAGE_CLASS_NAME = 'blocklyToastMessage'; +const CLOSE_BUTTON_CLASS_NAME = 'blocklyToastCloseButton'; + +/** + * Display/configuration options for a toast notification. + */ +export interface ToastOptions { + /** + * Toast ID. If set along with `oncePerSession`, will cause subsequent toasts + * with this ID to not be shown. + */ + id?: string; + + /** + * Flag to show the toast once per session only. + * Subsequent calls are ignored. + */ + oncePerSession?: boolean; + + /** + * Text of the message to display on the toast. + */ + message: string; + + /** + * Duration in seconds before the toast is removed. Defaults to 5. + */ + duration?: number; + + /** + * How prominently/interrupting the readout of the toast should be for + * screenreaders. Corresponds to aria-live and defaults to polite. + */ + assertiveness?: Toast.Assertiveness; +} + +/** + * Class that allows for showing and dismissing temporary notifications. + */ +export class Toast { + /** IDs of toasts that have previously been shown. */ + private static shownIds = new Set(); + + /** + * Shows a toast notification. + * + * @param workspace The workspace to show the toast on. + * @param options Configuration options for the toast message, duration, etc. + */ + static show(workspace: WorkspaceSvg, options: ToastOptions) { + if (options.oncePerSession && options.id) { + if (this.shownIds.has(options.id)) return; + this.shownIds.add(options.id); + } + + // Clear any existing toasts. + this.hide(workspace); + + const toast = this.createDom(workspace, options); + + // Animate the toast into view. + requestAnimationFrame(() => { + toast.style.bottom = '2rem'; + }); + } + + /** + * Creates the DOM representation of a toast. + * + * @param workspace The workspace to inject the toast notification onto. + * @param options Configuration options for the toast. + * @returns The root DOM element of the toast. + */ + protected static createDom(workspace: WorkspaceSvg, options: ToastOptions) { + const { + message, + duration = 5, + assertiveness = Toast.Assertiveness.POLITE, + } = options; + + const toast = document.createElement('div'); + workspace.getInjectionDiv().appendChild(toast); + toast.dataset.toastId = options.id; + toast.className = CLASS_NAME; + aria.setRole(toast, aria.Role.STATUS); + aria.setState(toast, aria.State.LIVE, assertiveness); + + const messageElement = toast.appendChild(document.createElement('div')); + messageElement.className = MESSAGE_CLASS_NAME; + messageElement.innerText = message; + const closeButton = toast.appendChild(document.createElement('button')); + closeButton.className = CLOSE_BUTTON_CLASS_NAME; + aria.setState(closeButton, aria.State.LABEL, Msg['CLOSE']); + const closeIcon = dom.createSvgElement( + Svg.SVG, + { + width: 24, + height: 24, + viewBox: '0 0 24 24', + fill: 'none', + }, + closeButton, + ); + aria.setState(closeIcon, aria.State.HIDDEN, true); + dom.createSvgElement( + Svg.RECT, + { + x: 19.7782, + y: 2.80762, + width: 2, + height: 24, + transform: 'rotate(45, 19.7782, 2.80762)', + fill: 'black', + }, + closeIcon, + ); + dom.createSvgElement( + Svg.RECT, + { + x: 2.80762, + y: 4.22183, + width: 2, + height: 24, + transform: 'rotate(-45, 2.80762, 4.22183)', + fill: 'black', + }, + closeIcon, + ); + closeButton.addEventListener('click', () => { + toast.remove(); + workspace.markFocused(); + }); + + let timeout: ReturnType; + const setToastTimeout = () => { + timeout = setTimeout(() => toast.remove(), duration * 1000); + }; + const clearToastTimeout = () => clearTimeout(timeout); + toast.addEventListener('focusin', clearToastTimeout); + toast.addEventListener('focusout', setToastTimeout); + toast.addEventListener('mouseenter', clearToastTimeout); + toast.addEventListener('mousemove', clearToastTimeout); + toast.addEventListener('mouseleave', setToastTimeout); + setToastTimeout(); + + return toast; + } + + /** + * Dismiss a toast, e.g. in response to a user action. + * + * @param workspace The workspace to dismiss a toast in. + * @param id The toast ID, or undefined to clear any toast. + */ + static hide(workspace: WorkspaceSvg, id?: string) { + const toast = workspace.getInjectionDiv().querySelector(`.${CLASS_NAME}`); + if (toast instanceof HTMLElement && (!id || id === toast.dataset.toastId)) { + toast.remove(); + } + } +} + +/** + * Options for how aggressively toasts should be read out by screenreaders. + * Values correspond to those for aria-live. + */ +export namespace Toast { + export enum Assertiveness { + ASSERTIVE = 'assertive', + POLITE = 'polite', + } +} + +Css.register(` +.${CLASS_NAME} { + font-size: 1.2rem; + position: absolute; + bottom: -10rem; + right: 2rem; + padding: 1rem; + color: black; + background-color: white; + border: 2px solid black; + border-radius: 0.4rem; + z-index: 999; + display: flex; + align-items: center; + gap: 0.8rem; + line-height: 1.5; + transition: bottom 0.3s ease-out; +} + +.${CLASS_NAME} .${MESSAGE_CLASS_NAME} { + maxWidth: 18rem; +} + +.${CLASS_NAME} .${CLOSE_BUTTON_CLASS_NAME} { + margin: 0; + padding: 0.2rem; + background-color: transparent; + color: black; + border: none; + cursor: pointer; +} +`); diff --git a/core/toolbox/category.ts b/packages/blockly/core/toolbox/category.ts similarity index 93% rename from core/toolbox/category.ts rename to packages/blockly/core/toolbox/category.ts index 1394f72187e..7b0db7b3fcd 100644 --- a/core/toolbox/category.ts +++ b/packages/blockly/core/toolbox/category.ts @@ -130,15 +130,15 @@ export class ToolboxCategory */ protected makeDefaultCssConfig_(): CssConfig { return { - 'container': 'blocklyToolboxCategory', - 'row': 'blocklyTreeRow', + 'container': 'blocklyToolboxCategoryContainer', + 'row': 'blocklyToolboxCategory', 'rowcontentcontainer': 'blocklyTreeRowContentContainer', - 'icon': 'blocklyTreeIcon', - 'label': 'blocklyTreeLabel', - 'contents': 'blocklyToolboxContents', - 'selected': 'blocklyTreeSelected', - 'openicon': 'blocklyTreeIconOpen', - 'closedicon': 'blocklyTreeIconClosed', + 'icon': 'blocklyToolboxCategoryIcon', + 'label': 'blocklyToolboxCategoryLabel', + 'contents': 'blocklyToolboxCategoryGroup', + 'selected': 'blocklyToolboxSelected', + 'openicon': 'blocklyToolboxCategoryIconOpen', + 'closedicon': 'blocklyToolboxCategoryIconClosed', }; } @@ -225,6 +225,10 @@ export class ToolboxCategory */ protected createContainer_(): HTMLDivElement { const container = document.createElement('div'); + // Ensure that the category has a tab index to ensure it receives focus when + // clicked (since clicking isn't managed by the toolbox). + container.tabIndex = -1; + container.id = this.getId(); const className = this.cssConfig_['container']; if (className) { dom.addClass(container, className); @@ -662,19 +666,19 @@ export type CssConfig = ToolboxCategory.CssConfig; /** CSS for Toolbox. See css.js for use. */ Css.register(` -.blocklyTreeRow:not(.blocklyTreeSelected):hover { +.blocklyToolboxCategory:not(.blocklyToolboxSelected):hover { background-color: rgba(255, 255, 255, .2); } -.blocklyToolboxDiv[layout="h"] .blocklyToolboxCategory { +.blocklyToolbox[layout="h"] .blocklyToolboxCategoryContainer { margin: 1px 5px 1px 0; } -.blocklyToolboxDiv[dir="RTL"][layout="h"] .blocklyToolboxCategory { +.blocklyToolbox[dir="RTL"][layout="h"] .blocklyToolboxCategoryContainer { margin: 1px 0 1px 5px; } -.blocklyTreeRow { +.blocklyToolboxCategory { height: 22px; line-height: 22px; margin-bottom: 3px; @@ -682,12 +686,12 @@ Css.register(` white-space: nowrap; } -.blocklyToolboxDiv[dir="RTL"] .blocklyTreeRow { +.blocklyToolbox[dir="RTL"] .blocklyToolboxCategory { margin-left: 8px; padding-right: 0; } -.blocklyTreeIcon { +.blocklyToolboxCategoryIcon { background-image: url(<<>>/sprites.png); height: 16px; vertical-align: middle; @@ -695,42 +699,42 @@ Css.register(` width: 16px; } -.blocklyTreeIconClosed { +.blocklyToolboxCategoryIconClosed { background-position: -32px -1px; } -.blocklyToolboxDiv[dir="RTL"] .blocklyTreeIconClosed { +.blocklyToolbox[dir="RTL"] .blocklyToolboxCategoryIconClosed { background-position: 0 -1px; } -.blocklyTreeSelected>.blocklyTreeIconClosed { +.blocklyToolboxSelected>.blocklyToolboxCategoryIconClosed { background-position: -32px -17px; } -.blocklyToolboxDiv[dir="RTL"] .blocklyTreeSelected>.blocklyTreeIconClosed { +.blocklyToolbox[dir="RTL"] .blocklyToolboxSelected>.blocklyToolboxCategoryIconClosed { background-position: 0 -17px; } -.blocklyTreeIconOpen { +.blocklyToolboxCategoryIconOpen { background-position: -16px -1px; } -.blocklyTreeSelected>.blocklyTreeIconOpen { +.blocklyToolboxSelected>.blocklyToolboxCategoryIconOpen { background-position: -16px -17px; } -.blocklyTreeLabel { +.blocklyToolboxCategoryLabel { cursor: default; font: 16px sans-serif; padding: 0 3px; vertical-align: middle; } -.blocklyToolboxDelete .blocklyTreeLabel { +.blocklyToolboxDelete .blocklyToolboxCategoryLabel { cursor: url("<<>>/handdelete.cur"), auto; } -.blocklyTreeSelected .blocklyTreeLabel { +.blocklyToolboxSelected .blocklyToolboxCategoryLabel { color: #fff; } `); diff --git a/core/toolbox/collapsible_category.ts b/packages/blockly/core/toolbox/collapsible_category.ts similarity index 99% rename from core/toolbox/collapsible_category.ts rename to packages/blockly/core/toolbox/collapsible_category.ts index 59143642502..5048ff1269d 100644 --- a/core/toolbox/collapsible_category.ts +++ b/packages/blockly/core/toolbox/collapsible_category.ts @@ -57,7 +57,7 @@ export class CollapsibleToolboxCategory override makeDefaultCssConfig_() { const cssConfig = super.makeDefaultCssConfig_(); - cssConfig['contents'] = 'blocklyToolboxContents'; + cssConfig['contents'] = 'blocklyToolboxCategoryGroup'; return cssConfig; } diff --git a/core/toolbox/separator.ts b/packages/blockly/core/toolbox/separator.ts similarity index 90% rename from core/toolbox/separator.ts rename to packages/blockly/core/toolbox/separator.ts index 23874e42e79..cd5ed245a04 100644 --- a/core/toolbox/separator.ts +++ b/packages/blockly/core/toolbox/separator.ts @@ -54,6 +54,10 @@ export class ToolboxSeparator extends ToolboxItem { */ protected createDom_(): HTMLDivElement { const container = document.createElement('div'); + // Ensure that the separator has a tab index to ensure it receives focus + // when clicked (since clicking isn't managed by the toolbox). + container.tabIndex = -1; + container.id = this.getId(); const className = this.cssConfig_['container']; if (className) { dom.addClass(container, className); @@ -87,7 +91,7 @@ Css.register(` margin: 5px 0; } -.blocklyToolboxDiv[layout="h"] .blocklyTreeSeparator { +.blocklyToolbox[layout="h"] .blocklyTreeSeparator { border-right: solid #e5e5e5 1px; border-bottom: none; height: auto; diff --git a/core/toolbox/toolbox.ts b/packages/blockly/core/toolbox/toolbox.ts similarity index 86% rename from core/toolbox/toolbox.ts rename to packages/blockly/core/toolbox/toolbox.ts index 12037839399..6f4daf4ed71 100644 --- a/core/toolbox/toolbox.ts +++ b/packages/blockly/core/toolbox/toolbox.ts @@ -13,7 +13,6 @@ // Unused import preserved for side-effects. Remove if unneeded. import {BlockSvg} from '../block_svg.js'; -import type {BlocklyOptions} from '../blockly_options.js'; import * as browserEvents from '../browser_events.js'; import * as common from '../common.js'; import {ComponentManager} from '../component_manager.js'; @@ -22,23 +21,26 @@ import {DeleteArea} from '../delete_area.js'; import '../events/events_toolbox_item_select.js'; import {EventType} from '../events/type.js'; import * as eventUtils from '../events/utils.js'; -import type {IAutoHideable} from '../interfaces/i_autohideable.js'; +import {getFocusManager} from '../focus_manager.js'; +import {type IAutoHideable} from '../interfaces/i_autohideable.js'; import type {ICollapsibleToolboxItem} from '../interfaces/i_collapsible_toolbox_item.js'; import {isDeletable} from '../interfaces/i_deletable.js'; import type {IDraggable} from '../interfaces/i_draggable.js'; import type {IFlyout} from '../interfaces/i_flyout.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; +import type {IFocusableTree} from '../interfaces/i_focusable_tree.js'; import type {IKeyboardAccessible} from '../interfaces/i_keyboard_accessible.js'; import type {ISelectableToolboxItem} from '../interfaces/i_selectable_toolbox_item.js'; import {isSelectableToolboxItem} from '../interfaces/i_selectable_toolbox_item.js'; import type {IStyleable} from '../interfaces/i_styleable.js'; import type {IToolbox} from '../interfaces/i_toolbox.js'; import type {IToolboxItem} from '../interfaces/i_toolbox_item.js'; -import {Options} from '../options.js'; import * as registry from '../registry.js'; import type {KeyboardShortcut} from '../shortcut_registry.js'; import * as Touch from '../touch.js'; import * as aria from '../utils/aria.js'; import * as dom from '../utils/dom.js'; +import * as idGenerator from '../utils/idgenerator.js'; import {Rect} from '../utils/rect.js'; import * as toolbox from '../utils/toolbox.js'; import type {WorkspaceSvg} from '../workspace_svg.js'; @@ -51,7 +53,12 @@ import {CollapsibleToolboxCategory} from './collapsible_category.js'; */ export class Toolbox extends DeleteArea - implements IAutoHideable, IKeyboardAccessible, IStyleable, IToolbox + implements + IAutoHideable, + IKeyboardAccessible, + IStyleable, + IToolbox, + IFocusableNode { /** * The unique ID for this component that is used to register with the @@ -70,9 +77,6 @@ export class Toolbox /** Whether the Toolbox is visible. */ protected isVisible_ = false; - /** The list of items in the toolbox. */ - protected contents_: IToolboxItem[] = []; - /** The width of the toolbox. */ protected width_ = 0; @@ -82,7 +86,10 @@ export class Toolbox /** The flyout for the toolbox. */ private flyout: IFlyout | null = null; - protected contentMap_: {[key: string]: IToolboxItem}; + + /** Map from ID to the corresponding toolbox item. */ + protected contents = new Map(); + toolboxPosition: toolbox.Position; /** The currently selected item. */ @@ -101,6 +108,9 @@ export class Toolbox /** The workspace this toolbox is on. */ protected readonly workspace_: WorkspaceSvg; + /** Whether the mouse is currently being clicked. */ + private mouseDown = false; + /** @param workspace The workspace in which to create new blocks. */ constructor(workspace: WorkspaceSvg) { super(); @@ -118,9 +128,6 @@ export class Toolbox /** Is RTL vs LTR. */ this.RTL = workspace.options.RTL; - /** A map from toolbox item IDs to toolbox items. */ - this.contentMap_ = Object.create(null); - /** Position of the toolbox and flyout relative to the workspace. */ this.toolboxPosition = workspace.options.toolboxPosition; } @@ -143,7 +150,9 @@ export class Toolbox this.flyout = this.createFlyout_(); this.HtmlDiv = this.createDom_(this.workspace_); - dom.insertAfter(this.flyout.createDom('svg'), svg); + const flyoutDom = this.flyout.createDom('svg'); + dom.addClass(flyoutDom, 'blocklyToolboxFlyout'); + dom.insertAfter(flyoutDom, svg); this.setVisible(true); this.flyout.init(workspace); @@ -164,6 +173,7 @@ export class Toolbox ComponentManager.Capability.DRAG_TARGET, ], }); + getFocusManager().registerTree(this, true); } /** @@ -176,9 +186,9 @@ export class Toolbox const svg = workspace.getParentSvg(); const container = this.createContainer_(); + container.id = idGenerator.getNextUniqueId(); this.contentsDiv_ = this.createContentsContainer_(); - this.contentsDiv_.tabIndex = 0; aria.setRole(this.contentsDiv_, aria.Role.TREE); container.appendChild(this.contentsDiv_); @@ -196,7 +206,7 @@ export class Toolbox protected createContainer_(): HTMLDivElement { const toolboxContainer = document.createElement('div'); toolboxContainer.setAttribute('layout', this.isHorizontal() ? 'h' : 'v'); - dom.addClass(toolboxContainer, 'blocklyToolboxDiv'); + dom.addClass(toolboxContainer, 'blocklyToolbox'); toolboxContainer.setAttribute('dir', this.RTL ? 'RTL' : 'LTR'); return toolboxContainer; } @@ -208,7 +218,7 @@ export class Toolbox */ protected createContentsContainer_(): HTMLDivElement { const contentsContainer = document.createElement('div'); - dom.addClass(contentsContainer, 'blocklyToolboxContents'); + dom.addClass(contentsContainer, 'blocklyToolboxCategoryGroup'); if (this.isHorizontal()) { contentsContainer.style.flexDirection = 'row'; } @@ -236,6 +246,16 @@ export class Toolbox ); this.boundEvents_.push(clickEvent); + const mouseUpEvent = browserEvents.bind( + container, + 'pointerup', + this, + () => { + this.mouseDown = false; + }, + ); + this.boundEvents_.push(mouseUpEvent); + const keyDownEvent = browserEvents.conditionalBind( contentsContainer, 'keydown', @@ -252,6 +272,7 @@ export class Toolbox * @param e Click event to handle. */ protected onClick_(e: PointerEvent) { + this.mouseDown = true; if (browserEvents.isRightButton(e) || e.target === this.HtmlDiv) { // Close flyout. (common.getMainWorkspace() as WorkspaceSvg).hideChaff(false); @@ -325,18 +346,7 @@ export class Toolbox */ protected createFlyout_(): IFlyout { const workspace = this.workspace_; - // TODO (#4247): Look into adding a makeFlyout method to Blockly Options. - const workspaceOptions = new Options({ - 'parentWorkspace': workspace, - 'rtl': workspace.RTL, - 'oneBasedIndex': workspace.options.oneBasedIndex, - 'horizontalLayout': workspace.horizontalLayout, - 'renderer': workspace.options.renderer, - 'rendererOverrides': workspace.options.rendererOverrides, - 'move': { - 'scrollbars': true, - }, - } as BlocklyOptions); + const workspaceOptions = workspace.copyOptionsForFlyout(); // Options takes in either 'end' or 'start'. This has already been parsed to // be either 0 or 1, so set it after. workspaceOptions.toolboxPosition = workspace.options.toolboxPosition; @@ -365,14 +375,8 @@ export class Toolbox */ render(toolboxDef: toolbox.ToolboxInfo) { this.toolboxDef_ = toolboxDef; - for (let i = 0; i < this.contents_.length; i++) { - const toolboxItem = this.contents_[i]; - if (toolboxItem) { - toolboxItem.dispose(); - } - } - this.contents_ = []; - this.contentMap_ = Object.create(null); + this.contents.forEach((item) => item.dispose()); + this.contents.clear(); this.renderContents_(toolboxDef['contents']); this.position(); this.handleToolboxItemResize(); @@ -443,8 +447,7 @@ export class Toolbox * @param toolboxItem The item in the toolbox. */ protected addToolboxItem_(toolboxItem: IToolboxItem) { - this.contents_.push(toolboxItem); - this.contentMap_[toolboxItem.getId()] = toolboxItem; + this.contents.set(toolboxItem.getId(), toolboxItem); if (toolboxItem.isCollapsible()) { const collapsibleItem = toolboxItem as ICollapsibleToolboxItem; const childToolboxItems = collapsibleItem.getChildToolboxItems(); @@ -461,7 +464,7 @@ export class Toolbox * @returns The list of items in the toolbox. */ getToolboxItems(): IToolboxItem[] { - return this.contents_; + return [...this.contents.values()]; } /** @@ -616,7 +619,7 @@ export class Toolbox * @returns The toolbox item with the given ID, or null if no item exists. */ getToolboxItemById(id: string): IToolboxItem | null { - return this.contentMap_[id] || null; + return this.contents.get(id) || null; } /** @@ -732,13 +735,18 @@ export class Toolbox // relative to the new absolute edge (ie toolbox edge). const workspace = this.workspace_; const rect = this.HtmlDiv!.getBoundingClientRect(); + const flyout = this.getFlyout(); const newX = this.toolboxPosition === toolbox.Position.LEFT - ? workspace.scrollX + rect.width + ? workspace.scrollX + + rect.width + + (flyout?.isVisible() ? flyout.getWidth() : 0) : workspace.scrollX; const newY = this.toolboxPosition === toolbox.Position.TOP - ? workspace.scrollY + rect.height + ? workspace.scrollY + + rect.height + + (flyout?.isVisible() ? flyout.getHeight() : 0) : workspace.scrollY; workspace.translate(newX, newY); @@ -758,14 +766,13 @@ export class Toolbox * @internal */ refreshTheme() { - for (let i = 0; i < this.contents_.length; i++) { - const child = this.contents_[i]; + this.contents.forEach((child) => { // TODO(#6097): Fix types or add refreshTheme to IToolboxItem. const childAsCategory = child as ToolboxCategory; if (childAsCategory.refreshTheme) { childAsCategory.refreshTheme(); } - } + }); } /** @@ -916,11 +923,9 @@ export class Toolbox * @param position The position of the item to select. */ selectItemByPosition(position: number) { - if (position > -1 && position < this.contents_.length) { - const item = this.contents_[position]; - if (item.isSelectable()) { - this.setSelectedItem(item); - } + const item = this.getToolboxItems()[position]; + if (item) { + this.setSelectedItem(item); } } @@ -1027,11 +1032,12 @@ export class Toolbox return false; } - let nextItemIdx = this.contents_.indexOf(this.selectedItem_) + 1; - if (nextItemIdx > -1 && nextItemIdx < this.contents_.length) { - let nextItem = this.contents_[nextItemIdx]; + const items = [...this.contents.values()]; + let nextItemIdx = items.indexOf(this.selectedItem_) + 1; + if (nextItemIdx > -1 && nextItemIdx < items.length) { + let nextItem = items[nextItemIdx]; while (nextItem && !nextItem.isSelectable()) { - nextItem = this.contents_[++nextItemIdx]; + nextItem = items[++nextItemIdx]; } if (nextItem && nextItem.isSelectable()) { this.setSelectedItem(nextItem); @@ -1051,11 +1057,12 @@ export class Toolbox return false; } - let prevItemIdx = this.contents_.indexOf(this.selectedItem_) - 1; - if (prevItemIdx > -1 && prevItemIdx < this.contents_.length) { - let prevItem = this.contents_[prevItemIdx]; + const items = [...this.contents.values()]; + let prevItemIdx = items.indexOf(this.selectedItem_) - 1; + if (prevItemIdx > -1 && prevItemIdx < items.length) { + let prevItem = items[prevItemIdx]; while (prevItem && !prevItem.isSelectable()) { - prevItem = this.contents_[--prevItemIdx]; + prevItem = items[--prevItemIdx]; } if (prevItem && prevItem.isSelectable()) { this.setSelectedItem(prevItem); @@ -1069,21 +1076,96 @@ export class Toolbox dispose() { this.workspace_.getComponentManager().removeComponent('toolbox'); this.flyout!.dispose(); - for (let i = 0; i < this.contents_.length; i++) { - const toolboxItem = this.contents_[i]; - toolboxItem.dispose(); - } + this.contents.forEach((item) => item.dispose()); for (let j = 0; j < this.boundEvents_.length; j++) { browserEvents.unbind(this.boundEvents_[j]); } this.boundEvents_ = []; - this.contents_ = []; + this.contents.clear(); if (this.HtmlDiv) { this.workspace_.getThemeManager().unsubscribe(this.HtmlDiv); dom.removeNode(this.HtmlDiv); } + + getFocusManager().unregisterTree(this); + } + + /** See IFocusableNode.getFocusableElement. */ + getFocusableElement(): HTMLElement | SVGElement { + if (!this.HtmlDiv) throw Error('Toolbox DOM has not yet been created.'); + return this.HtmlDiv; + } + + /** See IFocusableNode.getFocusableTree. */ + getFocusableTree(): IFocusableTree { + return this; + } + + /** See IFocusableNode.onNodeFocus. */ + onNodeFocus(): void {} + + /** See IFocusableNode.onNodeBlur. */ + onNodeBlur(): void {} + + /** See IFocusableNode.canBeFocused. */ + canBeFocused(): boolean { + return true; + } + + /** See IFocusableTree.getRootFocusableNode. */ + getRootFocusableNode(): IFocusableNode { + return this; + } + + /** See IFocusableTree.getRestoredFocusableNode. */ + getRestoredFocusableNode( + previousNode: IFocusableNode | null, + ): IFocusableNode | null { + // Always try to select the first selectable toolbox item rather than the + // root of the toolbox. + if (!previousNode || previousNode === this) { + return this.getToolboxItems().find((item) => item.isSelectable()) ?? null; + } + return null; + } + + /** See IFocusableTree.getNestedTrees. */ + getNestedTrees(): Array { + return []; + } + + /** See IFocusableTree.lookUpFocusableNode. */ + lookUpFocusableNode(id: string): IFocusableNode | null { + return this.getToolboxItemById(id) as IFocusableNode; + } + + /** See IFocusableTree.onTreeFocus. */ + onTreeFocus( + node: IFocusableNode, + _previousTree: IFocusableTree | null, + ): void { + if (node !== this) { + // Only select the item if it isn't already selected so as to not toggle. + // Also require that the mouse not be down, i.e. that the focusing of + // the toolbox was keyboard-driven, to avoid opening the flyout when + // clicking on an empty part of the toolbox. + if (this.getSelectedItem() !== node && !this.mouseDown) { + this.setSelectedItem(node as IToolboxItem); + } + } else { + this.clearSelection(); + } + } + + /** See IFocusableTree.onTreeBlur. */ + onTreeBlur(nextTree: IFocusableTree | null): void { + // If navigating to anything other than the toolbox's flyout then clear the + // selection so that the toolbox's flyout can automatically close. + if (!nextTree || nextTree !== this.flyout?.getWorkspace()) { + this.autoHide(false); + } } } @@ -1100,7 +1182,11 @@ Css.register(` } /* Category tree in Toolbox. */ -.blocklyToolboxDiv { +.blocklyToolbox { + box-sizing: border-box; + user-select: none; + -ms-user-select: none; + -webkit-user-select: none; background-color: #ddd; overflow-x: visible; overflow-y: auto; @@ -1110,13 +1196,13 @@ Css.register(` -webkit-tap-highlight-color: transparent; /* issue #1345 */ } -.blocklyToolboxContents { +.blocklyToolboxCategoryGroup { display: flex; flex-wrap: wrap; flex-direction: column; } -.blocklyToolboxContents:focus { +.blocklyToolboxCategoryGroup:focus { outline: none; } `); diff --git a/core/toolbox/toolbox_item.ts b/packages/blockly/core/toolbox/toolbox_item.ts similarity index 82% rename from core/toolbox/toolbox_item.ts rename to packages/blockly/core/toolbox/toolbox_item.ts index ef9d979ab43..9fc5c160ddc 100644 --- a/core/toolbox/toolbox_item.ts +++ b/packages/blockly/core/toolbox/toolbox_item.ts @@ -12,6 +12,7 @@ // Former goog.module ID: Blockly.ToolboxItem import type {ICollapsibleToolboxItem} from '../interfaces/i_collapsible_toolbox_item.js'; +import type {IFocusableTree} from '../interfaces/i_focusable_tree.js'; import type {IToolbox} from '../interfaces/i_toolbox.js'; import type {IToolboxItem} from '../interfaces/i_toolbox_item.js'; import * as idGenerator from '../utils/idgenerator.js'; @@ -148,5 +149,33 @@ export class ToolboxItem implements IToolboxItem { * @param _isVisible True if category should be visible. */ setVisible_(_isVisible: boolean) {} + + /** See IFocusableNode.getFocusableElement. */ + getFocusableElement(): HTMLElement | SVGElement { + const div = this.getDiv(); + if (!div) { + throw Error('Trying to access toolbox item before DOM is initialized.'); + } + if (!(div instanceof HTMLElement)) { + throw Error('Toolbox item div is unexpectedly not an HTML element.'); + } + return div as HTMLElement; + } + + /** See IFocusableNode.getFocusableTree. */ + getFocusableTree(): IFocusableTree { + return this.parentToolbox_; + } + + /** See IFocusableNode.onNodeFocus. */ + onNodeFocus(): void {} + + /** See IFocusableNode.onNodeBlur. */ + onNodeBlur(): void {} + + /** See IFocusableNode.canBeFocused. */ + canBeFocused(): boolean { + return true; + } } // nop by default diff --git a/core/tooltip.ts b/packages/blockly/core/tooltip.ts similarity index 100% rename from core/tooltip.ts rename to packages/blockly/core/tooltip.ts diff --git a/core/touch.ts b/packages/blockly/core/touch.ts similarity index 98% rename from core/touch.ts rename to packages/blockly/core/touch.ts index 8fb2cd2298c..b95e0408329 100644 --- a/core/touch.ts +++ b/packages/blockly/core/touch.ts @@ -49,7 +49,7 @@ export const TOUCH_MAP: {[key: string]: string[]} = { }; /** PID of queued long-press task. */ -let longPid_: AnyDuringMigration = 0; +let longPid_: ReturnType = 0; /** * Context menus on touch devices are activated using a long-press. diff --git a/core/trashcan.ts b/packages/blockly/core/trashcan.ts similarity index 97% rename from core/trashcan.ts rename to packages/blockly/core/trashcan.ts index dd0cbcccb67..b478307e1f0 100644 --- a/core/trashcan.ts +++ b/packages/blockly/core/trashcan.ts @@ -12,7 +12,6 @@ // Former goog.module ID: Blockly.Trashcan // Unused import preserved for side-effects. Remove if unneeded. -import type {BlocklyOptions} from './blockly_options.js'; import * as browserEvents from './browser_events.js'; import {ComponentManager} from './component_manager.js'; import {DeleteArea} from './delete_area.js'; @@ -26,7 +25,6 @@ import type {IDraggable} from './interfaces/i_draggable.js'; import type {IFlyout} from './interfaces/i_flyout.js'; import type {IPositionable} from './interfaces/i_positionable.js'; import type {UiMetrics} from './metrics_manager.js'; -import {Options} from './options.js'; import * as uiPosition from './positionable_helpers.js'; import * as registry from './registry.js'; import type * as blocks from './serialization/blocks.js'; @@ -103,17 +101,7 @@ export class Trashcan } // Create flyout options. - const flyoutWorkspaceOptions = new Options({ - 'scrollbars': true, - 'parentWorkspace': this.workspace, - 'rtl': this.workspace.RTL, - 'oneBasedIndex': this.workspace.options.oneBasedIndex, - 'renderer': this.workspace.options.renderer, - 'rendererOverrides': this.workspace.options.rendererOverrides, - 'move': { - 'scrollbars': true, - }, - } as BlocklyOptions); + const flyoutWorkspaceOptions = this.workspace.copyOptionsForFlyout(); // Create vertical or horizontal flyout. if (this.workspace.horizontalLayout) { flyoutWorkspaceOptions.toolboxPosition = @@ -239,10 +227,9 @@ export class Trashcan /** Initializes the trash can. */ init() { if (this.workspace.options.maxTrashcanContents > 0) { - dom.insertAfter( - this.flyout!.createDom(Svg.SVG)!, - this.workspace.getParentSvg(), - ); + const flyoutDom = this.flyout!.createDom(Svg.SVG)!; + dom.addClass(flyoutDom, 'blocklyTrashcanFlyout'); + dom.insertAfter(flyoutDom, this.workspace.getParentSvg()); this.flyout!.init(this.workspace); } this.workspace.getComponentManager().addComponent({ diff --git a/core/utils.ts b/packages/blockly/core/utils.ts similarity index 100% rename from core/utils.ts rename to packages/blockly/core/utils.ts diff --git a/core/utils/aria.ts b/packages/blockly/core/utils/aria.ts similarity index 91% rename from core/utils/aria.ts rename to packages/blockly/core/utils/aria.ts index 567ea95ef73..d997b8d0af0 100644 --- a/core/utils/aria.ts +++ b/packages/blockly/core/utils/aria.ts @@ -48,6 +48,12 @@ export enum Role { // ARIA role for a tree item that sometimes may be expanded or collapsed. TREEITEM = 'treeitem', + + // ARIA role for a visual separator in e.g. a menu. + SEPARATOR = 'separator', + + // ARIA role for a live region providing information. + STATUS = 'status', } /** @@ -107,6 +113,14 @@ export enum State { // ARIA property for slider minimum value. Value: number. VALUEMIN = 'valuemin', + + // ARIA property for live region chattiness. + // Value: one of {polite, assertive, off}. + LIVE = 'live', + + // ARIA property for removing elements from the accessibility tree. + // Value: one of {true, false, undefined}. + HIDDEN = 'hidden', } /** diff --git a/core/utils/array.ts b/packages/blockly/core/utils/array.ts similarity index 100% rename from core/utils/array.ts rename to packages/blockly/core/utils/array.ts diff --git a/core/utils/colour.ts b/packages/blockly/core/utils/colour.ts similarity index 100% rename from core/utils/colour.ts rename to packages/blockly/core/utils/colour.ts diff --git a/core/utils/coordinate.ts b/packages/blockly/core/utils/coordinate.ts similarity index 100% rename from core/utils/coordinate.ts rename to packages/blockly/core/utils/coordinate.ts diff --git a/core/utils/deprecation.ts b/packages/blockly/core/utils/deprecation.ts similarity index 100% rename from core/utils/deprecation.ts rename to packages/blockly/core/utils/deprecation.ts diff --git a/core/utils/dom.ts b/packages/blockly/core/utils/dom.ts similarity index 93% rename from core/utils/dom.ts rename to packages/blockly/core/utils/dom.ts index 309cd3fae3b..4087984151c 100644 --- a/core/utils/dom.ts +++ b/packages/blockly/core/utils/dom.ts @@ -208,16 +208,14 @@ export function getTextWidth(textElement: SVGTextElement): number { } } - // Attempt to compute fetch the width of the SVG text element. - try { - width = textElement.getComputedTextLength(); - } catch { - // In other cases where we fail to get the computed text. 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; - } + // Compute the width of the SVG text element. + const style = window.getComputedStyle(textElement); + width = getFastTextWidthWithSizeString( + textElement, + style.fontSize, + style.fontWeight, + style.fontFamily, + ); // Cache the computed width and return. if (cacheWidths) { @@ -291,13 +289,13 @@ export function getFastTextWidthWithSizeString( // Initialize the HTML canvas context and set the font. // The context font must match blocklyText's fontsize and font-family // set in CSS. - canvasContext = computeCanvas.getContext('2d') as CanvasRenderingContext2D; + canvasContext = computeCanvas.getContext('2d'); } - // Set the desired font size and family. - canvasContext.font = fontWeight + ' ' + fontSize + ' ' + fontFamily; // Measure the text width using the helper canvas context. - if (text) { + if (text && canvasContext) { + // Set the desired font size and family. + canvasContext.font = fontWeight + ' ' + fontSize + ' ' + fontFamily; width = canvasContext.measureText(text).width; } else { width = 0; diff --git a/core/utils/drag.ts b/packages/blockly/core/utils/drag.ts similarity index 100% rename from core/utils/drag.ts rename to packages/blockly/core/utils/drag.ts diff --git a/packages/blockly/core/utils/focusable_tree_traverser.ts b/packages/blockly/core/utils/focusable_tree_traverser.ts new file mode 100644 index 00000000000..aa4585b828d --- /dev/null +++ b/packages/blockly/core/utils/focusable_tree_traverser.ts @@ -0,0 +1,126 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; +import type {IFocusableTree} from '../interfaces/i_focusable_tree.js'; +import * as dom from '../utils/dom.js'; + +/** + * A helper utility for IFocusableTree implementations to aid with common + * tree traversals. + */ +export class FocusableTreeTraverser { + private static readonly ACTIVE_CLASS_NAME = 'blocklyActiveFocus'; + private static readonly PASSIVE_CSS_CLASS_NAME = 'blocklyPassiveFocus'; + private static readonly ACTIVE_FOCUS_NODE_CSS_SELECTOR = `.${FocusableTreeTraverser.ACTIVE_CLASS_NAME}`; + private static readonly PASSIVE_FOCUS_NODE_CSS_SELECTOR = `.${FocusableTreeTraverser.PASSIVE_CSS_CLASS_NAME}`; + + /** + * Returns the current IFocusableNode that is styled (and thus represented) as + * having either passive or active focus, only considering HTML and SVG + * elements. + * + * This can match against the tree's root. + * + * Note that this will never return a node from a nested sub-tree as that tree + * should specifically be used to retrieve its focused node. + * + * @param tree The IFocusableTree in which to search for a focused node. + * @returns The IFocusableNode currently with focus, or null if none. + */ + static findFocusedNode(tree: IFocusableTree): IFocusableNode | null { + const rootNode = tree.getRootFocusableNode(); + if (!rootNode.canBeFocused()) return null; + const root = rootNode.getFocusableElement(); + if ( + dom.hasClass(root, FocusableTreeTraverser.ACTIVE_CLASS_NAME) || + dom.hasClass(root, FocusableTreeTraverser.PASSIVE_CSS_CLASS_NAME) + ) { + // The root has focus. + return rootNode; + } + + const activeEl = root.querySelector(this.ACTIVE_FOCUS_NODE_CSS_SELECTOR); + if (activeEl instanceof HTMLElement || activeEl instanceof SVGElement) { + const active = FocusableTreeTraverser.findFocusableNodeFor( + activeEl, + tree, + ); + if (active) return active; + } + + // At most there should be one passive indicator per tree (not considering + // subtrees). + const passiveEl = root.querySelector(this.PASSIVE_FOCUS_NODE_CSS_SELECTOR); + if (passiveEl instanceof HTMLElement || passiveEl instanceof SVGElement) { + const passive = FocusableTreeTraverser.findFocusableNodeFor( + passiveEl, + tree, + ); + if (passive) return passive; + } + + return null; + } + + /** + * Returns the IFocusableNode corresponding to the specified HTML or SVG + * element iff it's the root element or a descendent of the root element of + * the specified IFocusableTree. + * + * If the element exists within the specified tree's DOM structure but does + * not directly correspond to a node, the nearest parent node (or the tree's + * root) will be returned to represent the provided element. + * + * If the tree contains another nested IFocusableTree, the nested tree may be + * traversed but its nodes will never be returned here per the contract of + * IFocusableTree.lookUpFocusableNode. + * + * The provided element must have a non-null, non-empty ID that conforms to + * the contract mentioned in IFocusableNode. + * + * @param element The HTML or SVG element being sought. + * @param tree The tree under which the provided element may be a descendant. + * @returns The matching IFocusableNode, or null if there is no match. + */ + static findFocusableNodeFor( + element: HTMLElement | SVGElement, + tree: IFocusableTree, + ): IFocusableNode | null { + // Note that the null check is due to Element.setAttribute() converting null + // to a string. + if (!element.id || element.id === 'null') return null; + + // First, match against subtrees. + const subTreeMatches = tree.getNestedTrees().map((tree) => { + return FocusableTreeTraverser.findFocusableNodeFor(element, tree); + }); + if (subTreeMatches.findIndex((match) => !!match) !== -1) { + // At least one subtree has a match for the element so it cannot be part + // of the outer tree. + return null; + } + + // Second, check against the tree's root. + const rootNode = tree.getRootFocusableNode(); + if (rootNode.canBeFocused() && element === rootNode.getFocusableElement()) { + return rootNode; + } + + // Third, check if the element has a node. + const matchedChildNode = tree.lookUpFocusableNode(element.id) ?? null; + if (matchedChildNode) return matchedChildNode; + + // Fourth, recurse up to find the nearest tree/node if it's possible. + const elementParent = element.parentElement; + if (!matchedChildNode && elementParent) { + return FocusableTreeTraverser.findFocusableNodeFor(elementParent, tree); + } + + // Otherwise, there's no matching node. + return null; + } +} diff --git a/core/utils/idgenerator.ts b/packages/blockly/core/utils/idgenerator.ts similarity index 100% rename from core/utils/idgenerator.ts rename to packages/blockly/core/utils/idgenerator.ts diff --git a/core/utils/keycodes.ts b/packages/blockly/core/utils/keycodes.ts similarity index 100% rename from core/utils/keycodes.ts rename to packages/blockly/core/utils/keycodes.ts diff --git a/core/utils/math.ts b/packages/blockly/core/utils/math.ts similarity index 100% rename from core/utils/math.ts rename to packages/blockly/core/utils/math.ts diff --git a/core/utils/metrics.ts b/packages/blockly/core/utils/metrics.ts similarity index 100% rename from core/utils/metrics.ts rename to packages/blockly/core/utils/metrics.ts diff --git a/core/utils/object.ts b/packages/blockly/core/utils/object.ts similarity index 66% rename from core/utils/object.ts rename to packages/blockly/core/utils/object.ts index 6aaabcc90f1..5cf0b49f2b9 100644 --- a/core/utils/object.ts +++ b/packages/blockly/core/utils/object.ts @@ -9,6 +9,9 @@ /** * Complete a deep merge of all members of a source object with a target object. * + * N.B. This is not a very sophisticated merge algorithm and does not + * handle complex cases. Use with caution. + * * @param target Target. * @param source Source. * @returns The resulting object. @@ -18,7 +21,9 @@ export function deepMerge( source: AnyDuringMigration, ): AnyDuringMigration { for (const x in source) { - if (source[x] !== null && typeof source[x] === 'object') { + if (source[x] !== null && Array.isArray(source[x])) { + target[x] = deepMerge(target[x] || [], source[x]); + } else if (source[x] !== null && typeof source[x] === 'object') { target[x] = deepMerge(target[x] || Object.create(null), source[x]); } else { target[x] = source[x]; diff --git a/core/utils/parsing.ts b/packages/blockly/core/utils/parsing.ts similarity index 100% rename from core/utils/parsing.ts rename to packages/blockly/core/utils/parsing.ts diff --git a/core/utils/rect.ts b/packages/blockly/core/utils/rect.ts similarity index 88% rename from core/utils/rect.ts rename to packages/blockly/core/utils/rect.ts index c7da2a6860b..5a6822633e1 100644 --- a/core/utils/rect.ts +++ b/packages/blockly/core/utils/rect.ts @@ -32,6 +32,16 @@ export class Rect { public right: number, ) {} + /** + * Converts a DOM or SVG Rect to a Blockly Rect. + * + * @param rect The rectangle to convert. + * @returns A representation of the same rectangle as a Blockly Rect. + */ + static from(rect: DOMRect | SVGRect): Rect { + return new Rect(rect.y, rect.y + rect.height, rect.x, rect.x + rect.width); + } + /** * Creates a new copy of this rectangle. * @@ -51,6 +61,11 @@ export class Rect { return this.right - this.left; } + /** Returns the top left coordinate of this rectangle. */ + getOrigin(): Coordinate { + return new Coordinate(this.left, this.top); + } + /** * Tests whether this rectangle contains a x/y coordinate. * diff --git a/core/utils/size.ts b/packages/blockly/core/utils/size.ts similarity index 100% rename from core/utils/size.ts rename to packages/blockly/core/utils/size.ts diff --git a/core/utils/string.ts b/packages/blockly/core/utils/string.ts similarity index 100% rename from core/utils/string.ts rename to packages/blockly/core/utils/string.ts diff --git a/core/utils/style.ts b/packages/blockly/core/utils/style.ts similarity index 100% rename from core/utils/style.ts rename to packages/blockly/core/utils/style.ts diff --git a/core/utils/svg.ts b/packages/blockly/core/utils/svg.ts similarity index 100% rename from core/utils/svg.ts rename to packages/blockly/core/utils/svg.ts diff --git a/core/utils/svg_math.ts b/packages/blockly/core/utils/svg_math.ts similarity index 100% rename from core/utils/svg_math.ts rename to packages/blockly/core/utils/svg_math.ts diff --git a/core/utils/svg_paths.ts b/packages/blockly/core/utils/svg_paths.ts similarity index 100% rename from core/utils/svg_paths.ts rename to packages/blockly/core/utils/svg_paths.ts diff --git a/core/utils/toolbox.ts b/packages/blockly/core/utils/toolbox.ts similarity index 99% rename from core/utils/toolbox.ts rename to packages/blockly/core/utils/toolbox.ts index 296bb6dcc94..f81ebdc72ca 100644 --- a/core/utils/toolbox.ts +++ b/packages/blockly/core/utils/toolbox.ts @@ -24,8 +24,6 @@ export interface BlockInfo { disabledReasons?: string[]; enabled?: boolean; id?: string; - x?: number; - y?: number; collapsed?: boolean; inline?: boolean; data?: string; diff --git a/core/utils/useragent.ts b/packages/blockly/core/utils/useragent.ts similarity index 100% rename from core/utils/useragent.ts rename to packages/blockly/core/utils/useragent.ts diff --git a/core/utils/xml.ts b/packages/blockly/core/utils/xml.ts similarity index 100% rename from core/utils/xml.ts rename to packages/blockly/core/utils/xml.ts diff --git a/core/variable_map.ts b/packages/blockly/core/variable_map.ts similarity index 51% rename from core/variable_map.ts rename to packages/blockly/core/variable_map.ts index b28e8a3550e..88c28cbc321 100644 --- a/core/variable_map.ts +++ b/packages/blockly/core/variable_map.ts @@ -17,14 +17,15 @@ import './events/events_var_delete.js'; import './events/events_var_rename.js'; import type {Block} from './block.js'; -import * as dialog from './dialog.js'; import {EventType} from './events/type.js'; import * as eventUtils from './events/utils.js'; -import {Msg} from './msg.js'; +import type {IVariableMap} from './interfaces/i_variable_map.js'; +import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js'; import {Names} from './names.js'; -import * as arrayUtils from './utils/array.js'; +import * as registry from './registry.js'; +import * as deprecation from './utils/deprecation.js'; import * as idGenerator from './utils/idgenerator.js'; -import {VariableModel} from './variable_model.js'; +import {deleteVariable, getVariableUsesById} from './variables.js'; import type {Workspace} from './workspace.js'; /** @@ -32,22 +33,34 @@ import type {Workspace} from './workspace.js'; * variable types as keys and lists of variables as values. The list of * variables are the type indicated by the key. */ -export class VariableMap { +export class VariableMap + implements IVariableMap> +{ /** - * A map from variable type to list of variable names. The lists contain + * A map from variable type to map of IDs to variables. The maps contain * all of the named variables in the workspace, including variables that are * not currently in use. */ - private variableMap = new Map(); + private variableMap = new Map< + string, + Map> + >(); - /** @param workspace The workspace this map belongs to. */ - constructor(public workspace: Workspace) {} + /** + * @param workspace The workspace this map belongs to. + * @param potentialMap True if this holds variables that don't exist in the + * workspace yet. + */ + constructor( + public workspace: Workspace, + public potentialMap = false, + ) {} /** Clear the variable map. Fires events for every deletion. */ clear() { for (const variables of this.variableMap.values()) { - while (variables.length > 0) { - this.deleteVariable(variables[0]); + for (const variable of variables.values()) { + this.deleteVariable(variable); } } if (this.variableMap.size !== 0) { @@ -61,16 +74,22 @@ export class VariableMap { * * @param variable Variable to rename. * @param newName New variable name. - * @internal + * @returns The newly renamed variable. */ - renameVariable(variable: VariableModel, newName: string) { - if (variable.name === newName) return; - const type = variable.type; + renameVariable( + variable: IVariableModel, + newName: string, + ): IVariableModel { + if (variable.getName() === newName) return variable; + const type = variable.getType(); const conflictVar = this.getVariable(newName, type); const blocks = this.workspace.getAllBlocks(false); - const existingGroup = eventUtils.getGroup(); - if (!existingGroup) { - eventUtils.setGroup(true); + let existingGroup = ''; + if (!this.potentialMap) { + existingGroup = eventUtils.getGroup(); + if (!existingGroup) { + eventUtils.setGroup(true); + } } try { // The IDs may match if the rename is a simple case change (name1 -> @@ -81,18 +100,56 @@ export class VariableMap { this.renameVariableWithConflict(variable, newName, conflictVar, blocks); } } finally { - eventUtils.setGroup(existingGroup); + if (!this.potentialMap) eventUtils.setGroup(existingGroup); + } + return variable; + } + + changeVariableType( + variable: IVariableModel, + newType: string, + ): IVariableModel { + const oldType = variable.getType(); + if (oldType === newType) return variable; + + const oldTypeVariables = this.variableMap.get(oldType); + oldTypeVariables?.delete(variable.getId()); + if (oldTypeVariables?.size === 0) { + this.variableMap.delete(oldType); + } + variable.setType(newType); + const newTypeVariables = + this.variableMap.get(newType) ?? + new Map>(); + newTypeVariables.set(variable.getId(), variable); + if (!this.variableMap.has(newType)) { + this.variableMap.set(newType, newTypeVariables); } + eventUtils.fire( + new (eventUtils.get(EventType.VAR_TYPE_CHANGE))( + variable, + oldType, + newType, + ), + ); + return variable; } /** * Rename a variable by updating its name in the variable map. Identify the * variable to rename with the given ID. * + * @deprecated v12: use VariableMap.renameVariable. * @param id ID of the variable to rename. * @param newName New variable name. */ renameVariableById(id: string, newName: string) { + deprecation.warn( + 'VariableMap.renameVariableById', + 'v12', + 'v13', + 'VariableMap.renameVariable', + ); const variable = this.getVariableById(id); if (!variable) { throw Error("Tried to rename a variable that didn't exist. ID: " + id); @@ -110,14 +167,16 @@ export class VariableMap { * @param blocks The list of all blocks in the workspace. */ private renameVariableAndUses( - variable: VariableModel, + variable: IVariableModel, newName: string, blocks: Block[], ) { - eventUtils.fire( - new (eventUtils.get(EventType.VAR_RENAME))(variable, newName), - ); - variable.name = newName; + if (!this.potentialMap) { + eventUtils.fire( + new (eventUtils.get(EventType.VAR_RENAME))(variable, newName), + ); + } + variable.setName(newName); for (let i = 0; i < blocks.length; i++) { blocks[i].updateVarName(variable); } @@ -135,13 +194,13 @@ export class VariableMap { * @param blocks The list of all blocks in the workspace. */ private renameVariableWithConflict( - variable: VariableModel, + variable: IVariableModel, newName: string, - conflictVar: VariableModel, + conflictVar: IVariableModel, blocks: Block[], ) { - const type = variable.type; - const oldCase = conflictVar.name; + const type = variable.getType(); + const oldCase = conflictVar.getName(); if (newName !== oldCase) { // Simple rename to change the case and update references. @@ -153,10 +212,12 @@ export class VariableMap { for (let i = 0; i < blocks.length; i++) { blocks[i].renameVarById(variable.getId(), conflictVar.getId()); } - // Finally delete the original variable, which is now unreferenced. - eventUtils.fire(new (eventUtils.get(EventType.VAR_DELETE))(variable)); - // And remove it from the list. - arrayUtils.removeElem(this.variableMap.get(type)!, variable); + if (!this.potentialMap) { + // Finally delete the original variable, which is now unreferenced. + eventUtils.fire(new (eventUtils.get(EventType.VAR_DELETE))(variable)); + } + // And remove it from the map. + this.variableMap.get(type)?.delete(variable.getId()); } /* End functions for renaming variables. */ @@ -173,9 +234,9 @@ export class VariableMap { */ createVariable( name: string, - opt_type?: string | null, - opt_id?: string | null, - ): VariableModel { + opt_type?: string, + opt_id?: string, + ): IVariableModel { let variable = this.getVariable(name, opt_type); if (variable) { if (opt_id && variable.getId() !== opt_id) { @@ -198,42 +259,78 @@ export class VariableMap { } const id = opt_id || idGenerator.genUid(); const type = opt_type || ''; + const VariableModel = registry.getClassFromOptions( + registry.Type.VARIABLE_MODEL, + this.workspace.options, + true, + ); + if (!VariableModel) { + throw new Error('No variable model is registered.'); + } variable = new VariableModel(this.workspace, name, type, id); - const variables = this.variableMap.get(type) || []; - variables.push(variable); - // Delete the list of variables of this type, and re-add it so that - // the most recent addition is at the end. - // This is used so the toolbox's set block is set to the most recent - // variable. - this.variableMap.delete(type); - this.variableMap.set(type, variables); - - eventUtils.fire(new (eventUtils.get(EventType.VAR_CREATE))(variable)); - + const variables = + this.variableMap.get(type) ?? + new Map>(); + variables.set(variable.getId(), variable); + if (!this.variableMap.has(type)) { + this.variableMap.set(type, variables); + } + if (!this.potentialMap) { + eventUtils.fire(new (eventUtils.get(EventType.VAR_CREATE))(variable)); + } return variable; } + /** + * Adds the given variable to this variable map. + * + * @param variable The variable to add. + */ + addVariable(variable: IVariableModel) { + const type = variable.getType(); + if (!this.variableMap.has(type)) { + this.variableMap.set( + type, + new Map>(), + ); + } + this.variableMap.get(type)?.set(variable.getId(), variable); + } + /* Begin functions for variable deletion. */ /** - * Delete a variable. + * Delete a variable and all of its uses without confirmation. * * @param variable Variable to delete. */ - deleteVariable(variable: VariableModel) { - const variableId = variable.getId(); - const variableList = this.variableMap.get(variable.type); - if (variableList) { - for (let i = 0; i < variableList.length; i++) { - const tempVar = variableList[i]; - if (tempVar.getId() === variableId) { - variableList.splice(i, 1); - eventUtils.fire(new (eventUtils.get(EventType.VAR_DELETE))(variable)); - if (variableList.length === 0) { - this.variableMap.delete(variable.type); - } - return; - } + deleteVariable(variable: IVariableModel) { + const uses = getVariableUsesById(this.workspace, variable.getId()); + let existingGroup = ''; + if (!this.potentialMap) { + existingGroup = eventUtils.getGroup(); + if (!existingGroup) { + eventUtils.setGroup(true); + } + } + try { + for (let i = 0; i < uses.length; i++) { + if (uses[i].isDeadOrDying()) continue; + + uses[i].dispose(true); + } + const variables = this.variableMap.get(variable.getType()); + if (!variables || !variables.has(variable.getId())) return; + variables.delete(variable.getId()); + if (!this.potentialMap) { + eventUtils.fire(new (eventUtils.get(EventType.VAR_DELETE))(variable)); + } + if (variables.size === 0) { + this.variableMap.delete(variable.getType()); + } + } finally { + if (!this.potentialMap) { + eventUtils.setGroup(existingGroup); } } } @@ -242,69 +339,22 @@ export class VariableMap { * Delete a variables by the passed in ID and all of its uses from this * workspace. May prompt the user for confirmation. * + * @deprecated v12: use Blockly.Variables.deleteVariable. * @param id ID of variable to delete. */ deleteVariableById(id: string) { + deprecation.warn( + 'VariableMap.deleteVariableById', + 'v12', + 'v13', + 'Blockly.Variables.deleteVariable', + ); const variable = this.getVariableById(id); if (variable) { - // Check whether this variable is a function parameter before deleting. - const variableName = variable.name; - const uses = this.getVariableUsesById(id); - for (let i = 0, block; (block = uses[i]); i++) { - if ( - block.type === 'procedures_defnoreturn' || - block.type === 'procedures_defreturn' - ) { - const procedureName = String(block.getFieldValue('NAME')); - const deleteText = Msg['CANNOT_DELETE_VARIABLE_PROCEDURE'] - .replace('%1', variableName) - .replace('%2', procedureName); - dialog.alert(deleteText); - return; - } - } - - if (uses.length > 1) { - // Confirm before deleting multiple blocks. - const confirmText = Msg['DELETE_VARIABLE_CONFIRMATION'] - .replace('%1', String(uses.length)) - .replace('%2', variableName); - dialog.confirm(confirmText, (ok) => { - if (ok && variable) { - this.deleteVariableInternal(variable, uses); - } - }); - } else { - // No confirmation necessary for a single block. - this.deleteVariableInternal(variable, uses); - } - } else { - console.warn("Can't delete non-existent variable: " + id); + deleteVariable(this.workspace, variable); } } - /** - * Deletes a variable and all of its uses from this workspace without asking - * the user for confirmation. - * - * @param variable Variable to delete. - * @param uses An array of uses of the variable. - * @internal - */ - deleteVariableInternal(variable: VariableModel, uses: Block[]) { - const existingGroup = eventUtils.getGroup(); - if (!existingGroup) { - eventUtils.setGroup(true); - } - try { - for (let i = 0; i < uses.length; i++) { - uses[i].dispose(true); - } - this.deleteVariable(variable); - } finally { - eventUtils.setGroup(existingGroup); - } - } /* End functions for variable deletion. */ /** * Find the variable by the given name and type and return it. Return null if @@ -315,17 +365,19 @@ export class VariableMap { * the empty string, which is a specific type. * @returns The variable with the given name, or null if it was not found. */ - getVariable(name: string, opt_type?: string | null): VariableModel | null { + getVariable( + name: string, + opt_type?: string, + ): IVariableModel | null { const type = opt_type || ''; - const list = this.variableMap.get(type); - if (list) { - for (let j = 0, variable; (variable = list[j]); j++) { - if (Names.equals(variable.name, name)) { - return variable; - } - } - } - return null; + const variables = this.variableMap.get(type); + if (!variables) return null; + + return ( + [...variables.values()].find((variable) => + Names.equals(variable.getName(), name), + ) ?? null + ); } /** @@ -334,12 +386,10 @@ export class VariableMap { * @param id The ID to check for. * @returns The variable with the given ID. */ - getVariableById(id: string): VariableModel | null { + getVariableById(id: string): IVariableModel | null { for (const variables of this.variableMap.values()) { - for (const variable of variables) { - if (variable.getId() === id) { - return variable; - } + if (variables.has(id)) { + return variables.get(id) ?? null; } } return null; @@ -353,36 +403,21 @@ export class VariableMap { * @returns The sought after variables of the passed in type. An empty array * if none are found. */ - getVariablesOfType(type: string | null): VariableModel[] { + getVariablesOfType(type: string | null): IVariableModel[] { type = type || ''; - const variableList = this.variableMap.get(type); - if (variableList) { - return variableList.slice(); - } - return []; + const variables = this.variableMap.get(type); + if (!variables) return []; + + return [...variables.values()]; } /** - * Return all variable and potential variable types. This list always - * contains the empty string. + * Returns a list of unique types of variables in this variable map. * - * @param ws The workspace used to look for potential variables. This can be - * different than the workspace stored on this object if the passed in ws - * is a flyout workspace. - * @returns List of variable types. - * @internal + * @returns A list of unique types of variables in this variable map. */ - getVariableTypes(ws: Workspace | null): string[] { - const variableTypes = new Set(this.variableMap.keys()); - if (ws && ws.getPotentialVariableMap()) { - for (const key of ws.getPotentialVariableMap()!.variableMap.keys()) { - variableTypes.add(key); - } - } - if (!variableTypes.has('')) { - variableTypes.add(''); - } - return Array.from(variableTypes.values()); + getTypes(): string[] { + return [...this.variableMap.keys()]; } /** @@ -390,10 +425,10 @@ export class VariableMap { * * @returns List of variable models. */ - getAllVariables(): VariableModel[] { - let allVariables: VariableModel[] = []; + getAllVariables(): IVariableModel[] { + let allVariables: IVariableModel[] = []; for (const variables of this.variableMap.values()) { - allVariables = allVariables.concat(variables); + allVariables = allVariables.concat(...variables.values()); } return allVariables; } @@ -401,34 +436,41 @@ export class VariableMap { /** * Returns all of the variable names of all types. * + * @deprecated v12: use Blockly.Variables.getAllVariables. * @returns All of the variable names of all types. */ getAllVariableNames(): string[] { - return Array.from(this.variableMap.values()) - .flat() - .map((variable) => variable.name); + deprecation.warn( + 'VariableMap.getAllVariableNames', + 'v12', + 'v13', + 'Blockly.Variables.getAllVariables', + ); + const names: string[] = []; + for (const variables of this.variableMap.values()) { + for (const variable of variables.values()) { + names.push(variable.getName()); + } + } + return names; } /** * Find all the uses of a named variable. * + * @deprecated v12: use Blockly.Variables.getVariableUsesById. * @param id ID of the variable to find. * @returns Array of block usages. */ getVariableUsesById(id: string): Block[] { - const uses = []; - const blocks = this.workspace.getAllBlocks(false); - // Iterate through every block and check the name. - for (let i = 0; i < blocks.length; i++) { - const blockVariables = blocks[i].getVarModels(); - if (blockVariables) { - for (let j = 0; j < blockVariables.length; j++) { - if (blockVariables[j].getId() === id) { - uses.push(blocks[i]); - } - } - } - } - return uses; + deprecation.warn( + 'VariableMap.getVariableUsesById', + 'v12', + 'v13', + 'Blockly.Variables.getVariableUsesById', + ); + return getVariableUsesById(this.workspace, id); } } + +registry.register(registry.Type.VARIABLE_MAP, registry.DEFAULT, VariableMap); diff --git a/packages/blockly/core/variable_model.ts b/packages/blockly/core/variable_model.ts new file mode 100644 index 00000000000..4cd16a9c321 --- /dev/null +++ b/packages/blockly/core/variable_model.ts @@ -0,0 +1,150 @@ +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Components for the variable model. + * + * @class + */ +// Former goog.module ID: Blockly.VariableModel + +// Unused import preserved for side-effects. Remove if unneeded. +import './events/events_var_create.js'; + +import {EventType} from './events/type.js'; +import * as eventUtils from './events/utils.js'; +import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js'; +import * as registry from './registry.js'; +import * as idGenerator from './utils/idgenerator.js'; +import type {Workspace} from './workspace.js'; + +/** + * Class for a variable model. + * Holds information for the variable including name, ID, and type. + * + * @see {Blockly.FieldVariable} + */ +export class VariableModel implements IVariableModel { + private type: string; + private readonly id: string; + + /** + * @param workspace The variable's workspace. + * @param name The name of the variable. This is the user-visible name (e.g. + * 'my var' or '私の変数'), not the generated name. + * @param opt_type The type of the variable like 'int' or 'string'. + * Does not need to be unique. Field_variable can filter variables based + * on their type. This will default to '' which is a specific type. + * @param opt_id The unique ID of the variable. This will default to a UUID. + */ + constructor( + private readonly workspace: Workspace, + private name: string, + opt_type?: string, + opt_id?: string, + ) { + /** + * The type of the variable, such as 'int' or 'sound_effect'. This may be + * used to build a list of variables of a specific type. By default this is + * the empty string '', which is a specific type. + * + * @see {Blockly.FieldVariable} + */ + this.type = opt_type || ''; + + /** + * A unique ID for the variable. This should be defined at creation and + * not change, even if the name changes. In most cases this should be a + * UUID. + */ + this.id = opt_id || idGenerator.genUid(); + } + + /** @returns The ID for the variable. */ + getId(): string { + return this.id; + } + + /** @returns The name of this variable. */ + getName(): string { + return this.name; + } + + /** + * Updates the user-visible name of this variable. + * + * @returns The newly-updated variable. + */ + setName(newName: string): this { + this.name = newName; + return this; + } + + /** @returns The type of this variable. */ + getType(): string { + return this.type; + } + + /** + * Updates the type of this variable. + * + * @returns The newly-updated variable. + */ + setType(newType: string): this { + this.type = newType; + return this; + } + + /** + * Returns the workspace this VariableModel belongs to. + * + * @returns The workspace this VariableModel belongs to. + */ + getWorkspace(): Workspace { + return this.workspace; + } + + /** + * Serializes this VariableModel. + * + * @returns a JSON representation of this VariableModel. + */ + save(): IVariableState { + const state: IVariableState = { + 'name': this.getName(), + 'id': this.getId(), + }; + const type = this.getType(); + if (type) { + state['type'] = type; + } + + return state; + } + + /** + * Loads the persisted state into a new variable in the given workspace. + * + * @param state The serialized state of a variable model from save(). + * @param workspace The workspace to create the new variable in. + */ + static load(state: IVariableState, workspace: Workspace) { + const variable = new this( + workspace, + state['name'], + state['type'], + state['id'], + ); + workspace.getVariableMap().addVariable(variable); + eventUtils.fire(new (eventUtils.get(EventType.VAR_CREATE))(variable)); + } +} + +registry.register( + registry.Type.VARIABLE_MODEL, + registry.DEFAULT, + VariableModel, +); diff --git a/core/variables.ts b/packages/blockly/core/variables.ts similarity index 67% rename from core/variables.ts rename to packages/blockly/core/variables.ts index 491b4c1b758..cbbd8843fa1 100644 --- a/core/variables.ts +++ b/packages/blockly/core/variables.ts @@ -6,13 +6,16 @@ // Former goog.module ID: Blockly.Variables +import type {Block} from './block.js'; import {Blocks} from './blocks.js'; import * as dialog from './dialog.js'; import {isLegacyProcedureDefBlock} from './interfaces/i_legacy_procedure_blocks.js'; import {isVariableBackedParameterModel} from './interfaces/i_variable_backed_parameter_model.js'; +import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js'; import {Msg} from './msg.js'; +import * as deprecation from './utils/deprecation.js'; +import type {BlockInfo, FlyoutItemInfo} from './utils/toolbox.js'; import * as utilsXml from './utils/xml.js'; -import {VariableModel} from './variable_model.js'; import type {Workspace} from './workspace.js'; import type {WorkspaceSvg} from './workspace_svg.js'; @@ -28,15 +31,18 @@ export const CATEGORY_NAME = 'VARIABLE'; /** * Find all user-created variables that are in use in the workspace. * For use by generators. + * * To get a list of all variables on a workspace, including unused variables, - * call Workspace.getAllVariables. + * call getAllVariables. * * @param ws The workspace to search for variables. * @returns Array of variable models. */ -export function allUsedVarModels(ws: Workspace): VariableModel[] { +export function allUsedVarModels( + ws: Workspace, +): IVariableModel[] { const blocks = ws.getAllBlocks(false); - const variables = new Set(); + const variables = new Set>(); // Iterate through every block and add each variable to the set. for (let i = 0; i < blocks.length; i++) { const blockVariables = blocks[i].getVarModels(); @@ -56,6 +62,7 @@ export function allUsedVarModels(ws: Workspace): VariableModel[] { /** * Find all developer variables used by blocks in the workspace. + * * Developer variables are never shown to the user, but are declared as global * variables in the generated code. * To declare developer variables, define the getDeveloperVariables function on @@ -81,6 +88,157 @@ export function allDeveloperVariables(workspace: Workspace): string[] { return Array.from(variables.values()); } +/** + * Internal wrapper that returns the contents of the variables category. + * + * @internal + * @param workspace The workspace to populate variable blocks for. + */ +export function internalFlyoutCategory( + workspace: WorkspaceSvg, +): FlyoutItemInfo[] { + return flyoutCategory(workspace, false); +} + +export function flyoutCategory( + workspace: WorkspaceSvg, + useXml: true, +): Element[]; +export function flyoutCategory( + workspace: WorkspaceSvg, + useXml: false, +): FlyoutItemInfo[]; +/** + * Construct the elements (blocks and button) required by the flyout for the + * variable category. + * + * @param workspace The workspace containing variables. + * @param useXml True to return the contents as XML, false to use JSON. + * @returns List of flyout contents as either XML or JSON. + */ +export function flyoutCategory( + workspace: WorkspaceSvg, + useXml = true, +): Element[] | FlyoutItemInfo[] { + if (!Blocks['variables_set'] && !Blocks['variables_get']) { + console.warn( + 'There are no variable blocks, but there is a variable category.', + ); + } + + if (useXml) { + deprecation.warn( + 'The XML return value of Blockly.Variables.flyoutCategory()', + 'v12', + 'v13', + 'the same method, but handle a return type of FlyoutItemInfo[] (JSON) instead.', + ); + return xmlFlyoutCategory(workspace); + } + + workspace.registerButtonCallback('CREATE_VARIABLE', function (button) { + createVariableButtonHandler(button.getTargetWorkspace()); + }); + + return [ + { + 'kind': 'button', + 'text': '%{BKY_NEW_VARIABLE}', + 'callbackkey': 'CREATE_VARIABLE', + }, + ...jsonFlyoutCategoryBlocks( + workspace, + workspace.getVariableMap().getVariablesOfType(''), + true, + ), + ]; +} + +/** + * Returns the JSON definition for a variable field. + * + * @param variable The variable the field should reference. + * @returns JSON for a variable field. + */ +function generateVariableFieldJson(variable: IVariableModel) { + return { + 'VAR': { + 'name': variable.getName(), + 'type': variable.getType(), + }, + }; +} + +/** + * Construct the blocks required by the flyout for the variable category. + * + * @internal + * @param workspace The workspace containing variables. + * @param variables List of variables to create blocks for. + * @param includeChangeBlocks True to include `change x by _` blocks. + * @param getterType The type of the variable getter block to generate. + * @param setterType The type of the variable setter block to generate. + * @returns JSON list of blocks. + */ +export function jsonFlyoutCategoryBlocks( + workspace: Workspace, + variables: IVariableModel[], + includeChangeBlocks: boolean, + getterType = 'variables_get', + setterType = 'variables_set', +): BlockInfo[] { + includeChangeBlocks &&= Blocks['math_change']; + + const blocks = []; + const mostRecentVariable = variables.slice(-1)[0]; + if (mostRecentVariable) { + // Show one setter block, with the name of the most recently created variable. + if (Blocks[setterType]) { + blocks.push({ + kind: 'block', + type: setterType, + gap: includeChangeBlocks ? 8 : 24, + fields: generateVariableFieldJson(mostRecentVariable), + }); + } + + if (includeChangeBlocks) { + blocks.push({ + 'kind': 'block', + 'type': 'math_change', + 'gap': Blocks[getterType] ? 20 : 8, + 'fields': generateVariableFieldJson(mostRecentVariable), + 'inputs': { + 'DELTA': { + 'shadow': { + 'type': 'math_number', + 'fields': { + 'NUM': 1, + }, + }, + }, + }, + }); + } + } + + if (Blocks[getterType]) { + // Show one getter block for each variable, sorted in alphabetical order. + blocks.push( + ...variables.sort(compareByName).map((variable) => { + return { + 'kind': 'block', + 'type': getterType, + 'gap': 8, + 'fields': generateVariableFieldJson(variable), + }; + }), + ); + } + + return blocks; +} + /** * Construct the elements (blocks and button) required by the flyout for the * variable category. @@ -88,7 +246,7 @@ export function allDeveloperVariables(workspace: Workspace): string[] { * @param workspace The workspace containing variables. * @returns Array of XML elements. */ -export function flyoutCategory(workspace: WorkspaceSvg): Element[] { +function xmlFlyoutCategory(workspace: WorkspaceSvg): Element[] { let xmlList = new Array(); const button = document.createElement('button'); button.setAttribute('text', '%{BKY_NEW_VARIABLE}'); @@ -112,7 +270,7 @@ export function flyoutCategory(workspace: WorkspaceSvg): Element[] { * @returns Array of XML block elements. */ export function flyoutCategoryBlocks(workspace: Workspace): Element[] { - const variableModelList = workspace.getVariablesOfType(''); + const variableModelList = workspace.getVariableMap().getVariablesOfType(''); const xmlList = []; if (variableModelList.length > 0) { @@ -142,7 +300,7 @@ export function flyoutCategoryBlocks(workspace: Workspace): Element[] { } if (Blocks['variables_get']) { - variableModelList.sort(VariableModel.compareByName); + variableModelList.sort(compareByName); for (let i = 0, variable; (variable = variableModelList[i]); i++) { const block = utilsXml.createElement('block'); block.setAttribute('type', 'variables_get'); @@ -176,7 +334,10 @@ export function generateUniqueName(workspace: Workspace): string { function generateUniqueNameInternal(workspace: Workspace): string { return generateUniqueNameFromOptions( VAR_LETTER_OPTIONS.charAt(0), - workspace.getAllVariableNames(), + workspace + .getVariableMap() + .getAllVariables() + .map((v) => v.getName()), ); } @@ -259,17 +420,19 @@ export function createVariableButtonHandler( const existing = nameUsedWithAnyType(text, workspace); if (!existing) { // No conflict - workspace.createVariable(text, type); + workspace.getVariableMap().createVariable(text, type); if (opt_callback) opt_callback(text); return; } let msg; - if (existing.type === type) { - msg = Msg['VARIABLE_ALREADY_EXISTS'].replace('%1', existing.name); + if (existing.getType() === type) { + msg = Msg['VARIABLE_ALREADY_EXISTS'].replace('%1', existing.getName()); } else { msg = Msg['VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE']; - msg = msg.replace('%1', existing.name).replace('%2', existing.type); + msg = msg + .replace('%1', existing.getName()) + .replace('%2', existing.getType()); } dialog.alert(msg, function () { promptAndCheckWithAlert(text); @@ -292,14 +455,14 @@ export function createVariableButtonHandler( */ export function renameVariable( workspace: Workspace, - variable: VariableModel, + variable: IVariableModel, opt_callback?: (p1?: string | null) => void, ) { // This function needs to be named so it can be called recursively. function promptAndCheckWithAlert(defaultName: string) { const promptText = Msg['RENAME_VARIABLE_TITLE'].replace( '%1', - variable.name, + variable.getName(), ); promptName(promptText, defaultName, function (newName) { if (!newName) { @@ -308,15 +471,19 @@ export function renameVariable( return; } - const existing = nameUsedWithOtherType(newName, variable.type, workspace); + const existing = nameUsedWithOtherType( + newName, + variable.getType(), + workspace, + ); const procedure = nameUsedWithConflictingParam( - variable.name, + variable.getName(), newName, workspace, ); if (!existing && !procedure) { // No conflict. - workspace.renameVariableById(variable.getId(), newName); + workspace.getVariableMap().renameVariable(variable, newName); if (opt_callback) opt_callback(newName); return; } @@ -324,8 +491,8 @@ export function renameVariable( let msg = ''; if (existing) { msg = Msg['VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE'] - .replace('%1', existing.name) - .replace('%2', existing.type); + .replace('%1', existing.getName()) + .replace('%2', existing.getType()); } else if (procedure) { msg = Msg['VARIABLE_ALREADY_EXISTS_FOR_A_PARAMETER'] .replace('%1', newName) @@ -379,12 +546,15 @@ function nameUsedWithOtherType( name: string, type: string, workspace: Workspace, -): VariableModel | null { +): IVariableModel | null { const allVariables = workspace.getVariableMap().getAllVariables(); name = name.toLowerCase(); for (let i = 0, variable; (variable = allVariables[i]); i++) { - if (variable.name.toLowerCase() === name && variable.type !== type) { + if ( + variable.getName().toLowerCase() === name && + variable.getType() !== type + ) { return variable; } } @@ -401,12 +571,12 @@ function nameUsedWithOtherType( export function nameUsedWithAnyType( name: string, workspace: Workspace, -): VariableModel | null { +): IVariableModel | null { const allVariables = workspace.getVariableMap().getAllVariables(); name = name.toLowerCase(); for (let i = 0, variable; (variable = allVariables[i]); i++) { - if (variable.name.toLowerCase() === name) { + if (variable.getName().toLowerCase() === name) { return variable; } } @@ -452,7 +622,7 @@ function checkForConflictingParamWithProcedureModels( const params = procedure .getParameters() .filter(isVariableBackedParameterModel) - .map((param) => param.getVariableModel().name); + .map((param) => param.getVariableModel().getName()); if (!params) continue; const procHasOld = params.some((param) => param.toLowerCase() === oldName); const procHasNew = params.some((param) => param.toLowerCase() === newName); @@ -492,7 +662,7 @@ function checkForConflictingParamWithLegacyProcedures( * @returns The generated DOM. */ export function generateVariableFieldDom( - variableModel: VariableModel, + variableModel: IVariableModel, ): Element { /* Generates the following XML: * foo @@ -500,8 +670,8 @@ export function generateVariableFieldDom( const field = utilsXml.createElement('field'); field.setAttribute('name', 'VAR'); field.setAttribute('id', variableModel.getId()); - field.setAttribute('variabletype', variableModel.type); - const name = utilsXml.createTextNode(variableModel.name); + field.setAttribute('variabletype', variableModel.getType()); + const name = utilsXml.createTextNode(variableModel.getName()); field.appendChild(name); return field; } @@ -523,7 +693,7 @@ export function getOrCreateVariablePackage( id: string | null, opt_name?: string, opt_type?: string, -): VariableModel { +): IVariableModel { let variable = getVariable(workspace, id, opt_name, opt_type); if (!variable) { variable = createVariable(workspace, id, opt_name, opt_type); @@ -551,13 +721,13 @@ export function getVariable( id: string | null, opt_name?: string, opt_type?: string, -): VariableModel | null { +): IVariableModel | null { const potentialVariableMap = workspace.getPotentialVariableMap(); let variable = null; // Try to just get the variable, by ID if possible. if (id) { // Look in the real variable map before checking the potential variable map. - variable = workspace.getVariableById(id); + variable = workspace.getVariableMap().getVariableById(id); if (!variable && potentialVariableMap) { variable = potentialVariableMap.getVariableById(id); } @@ -572,7 +742,7 @@ export function getVariable( throw Error('Tried to look up a variable by name without a type'); } // Otherwise look up by name and type. - variable = workspace.getVariable(opt_name, opt_type); + variable = workspace.getVariableMap().getVariable(opt_name, opt_type); if (!variable && potentialVariableMap) { variable = potentialVariableMap.getVariable(opt_name, opt_type); } @@ -596,7 +766,8 @@ function createVariable( id: string | null, opt_name?: string, opt_type?: string, -): VariableModel { +): IVariableModel { + const variableMap = workspace.getVariableMap(); const potentialVariableMap = workspace.getPotentialVariableMap(); // Variables without names get uniquely named for this workspace. if (!opt_name) { @@ -609,10 +780,14 @@ function createVariable( // Create a potential variable if in the flyout. let variable = null; if (potentialVariableMap) { - variable = potentialVariableMap.createVariable(opt_name, opt_type, id); + variable = potentialVariableMap.createVariable( + opt_name, + opt_type, + id ?? undefined, + ); } else { // In the main workspace, create a real variable. - variable = workspace.createVariable(opt_name, opt_type, id); + variable = variableMap.createVariable(opt_name, opt_type, id); } return variable; } @@ -632,9 +807,9 @@ function createVariable( */ export function getAddedVariables( workspace: Workspace, - originalVariables: VariableModel[], -): VariableModel[] { - const allCurrentVariables = workspace.getAllVariables(); + originalVariables: IVariableModel[], +): IVariableModel[] { + const allCurrentVariables = workspace.getVariableMap().getAllVariables(); const addedVariables = []; if (originalVariables.length !== allCurrentVariables.length) { for (let i = 0; i < allCurrentVariables.length; i++) { @@ -649,6 +824,108 @@ export function getAddedVariables( return addedVariables; } +/** + * A custom compare function for the VariableModel objects. + * + * @param var1 First variable to compare. + * @param var2 Second variable to compare. + * @returns -1 if name of var1 is less than name of var2, 0 if equal, and 1 if + * greater. + * @internal + */ +export function compareByName( + var1: IVariableModel, + var2: IVariableModel, +): number { + return var1 + .getName() + .localeCompare(var2.getName(), undefined, {sensitivity: 'base'}); +} + +/** + * Find all the uses of a named variable. + * + * @param workspace The workspace to search for the variable. + * @param id ID of the variable to find. + * @returns Array of block usages. + */ +export function getVariableUsesById(workspace: Workspace, id: string): Block[] { + const uses = []; + const blocks = workspace.getAllBlocks(false); + // Iterate through every block and check the name. + for (let i = 0; i < blocks.length; i++) { + const blockVariables = blocks[i].getVarModels(); + if (blockVariables) { + for (let j = 0; j < blockVariables.length; j++) { + if (blockVariables[j].getId() === id) { + uses.push(blocks[i]); + } + } + } + } + return uses; +} + +/** + * Delete a variable and all of its uses from the given workspace. May prompt + * the user for confirmation. + * + * @param workspace The workspace from which to delete the variable. + * @param variable The variable to delete. + * @param triggeringBlock The block from which this deletion was triggered, if + * any. Used to exclude it from checking and warning about blocks + * referencing the variable being deleted. + */ +export function deleteVariable( + workspace: Workspace, + variable: IVariableModel, + triggeringBlock?: Block, +) { + // Check whether this variable is a function parameter before deleting. + const variableName = variable.getName(); + const uses = getVariableUsesById(workspace, variable.getId()); + for (let i = uses.length - 1; i >= 0; i--) { + const block = uses[i]; + if ( + block.type === 'procedures_defnoreturn' || + block.type === 'procedures_defreturn' + ) { + const procedureName = String(block.getFieldValue('NAME')); + const deleteText = Msg['CANNOT_DELETE_VARIABLE_PROCEDURE'] + .replace('%1', variableName) + .replace('%2', procedureName); + dialog.alert(deleteText); + return; + } + if (block === triggeringBlock) { + uses.splice(i, 1); + } + } + + if ((triggeringBlock && uses.length) || uses.length > 1) { + // Confirm before deleting multiple blocks. + const confirmText = Msg['DELETE_VARIABLE_CONFIRMATION'] + .replace( + '%1', + String( + uses.length + + (triggeringBlock && !triggeringBlock.workspace.isFlyout ? 1 : 0), + ), + ) + .replace('%2', variableName); + dialog.confirm(confirmText, (ok) => { + if (ok && variable) { + workspace.getVariableMap().deleteVariable(variable); + } + }); + } else { + // No confirmation necessary when the block that triggered the deletion is + // the only block referencing this variable or if only one block referencing + // this variable exists and the deletion was triggered programmatically. + workspace.getVariableMap().deleteVariable(variable); + } +} + export const TEST_ONLY = { generateUniqueNameInternal, }; diff --git a/core/variables_dynamic.ts b/packages/blockly/core/variables_dynamic.ts similarity index 62% rename from core/variables_dynamic.ts rename to packages/blockly/core/variables_dynamic.ts index 9788962c7b2..f8169d28123 100644 --- a/core/variables_dynamic.ts +++ b/packages/blockly/core/variables_dynamic.ts @@ -9,8 +9,9 @@ import {Blocks} from './blocks.js'; import type {FlyoutButton} from './flyout_button.js'; import {Msg} from './msg.js'; +import * as deprecation from './utils/deprecation.js'; +import type {FlyoutItemInfo} from './utils/toolbox.js'; import * as xml from './utils/xml.js'; -import {VariableModel} from './variable_model.js'; import * as Variables from './variables.js'; import type {Workspace} from './workspace.js'; import type {WorkspaceSvg} from './workspace_svg.js'; @@ -69,6 +70,92 @@ function colourButtonClickHandler(button: FlyoutButton) { // eslint-disable-next-line camelcase export const onCreateVariableButtonClick_Colour = colourButtonClickHandler; +/** + * Internal wrapper that returns the contents of the dynamic variables category. + * + * @internal + * @param workspace The workspace to populate variable blocks for. + */ +export function internalFlyoutCategory( + workspace: WorkspaceSvg, +): FlyoutItemInfo[] { + return flyoutCategory(workspace, false); +} + +export function flyoutCategory( + workspace: WorkspaceSvg, + useXml: true, +): Element[]; +export function flyoutCategory( + workspace: WorkspaceSvg, + useXml: false, +): FlyoutItemInfo[]; +/** + * Construct the elements (blocks and button) required by the flyout for the + * dynamic variables category. + * + * @param useXml True to return the contents as XML, false to use JSON. + * @returns List of flyout contents as either XML or JSON. + */ +export function flyoutCategory( + workspace: WorkspaceSvg, + useXml = true, +): Element[] | FlyoutItemInfo[] { + if (!Blocks['variables_set_dynamic'] && !Blocks['variables_get_dynamic']) { + console.warn( + 'There are no dynamic variable blocks, but there is a dynamic variable category.', + ); + } + + if (useXml) { + deprecation.warn( + 'The XML return value of Blockly.VariablesDynamic.flyoutCategory()', + 'v12', + 'v13', + 'the same method, but handle a return type of FlyoutItemInfo[] (JSON) instead.', + ); + return xmlFlyoutCategory(workspace); + } + + workspace.registerButtonCallback( + 'CREATE_VARIABLE_STRING', + stringButtonClickHandler, + ); + workspace.registerButtonCallback( + 'CREATE_VARIABLE_NUMBER', + numberButtonClickHandler, + ); + workspace.registerButtonCallback( + 'CREATE_VARIABLE_COLOUR', + colourButtonClickHandler, + ); + + return [ + { + 'kind': 'button', + 'text': Msg['NEW_STRING_VARIABLE'], + 'callbackkey': 'CREATE_VARIABLE_STRING', + }, + { + 'kind': 'button', + 'text': Msg['NEW_NUMBER_VARIABLE'], + 'callbackkey': 'CREATE_VARIABLE_NUMBER', + }, + { + 'kind': 'button', + 'text': Msg['NEW_COLOUR_VARIABLE'], + 'callbackkey': 'CREATE_VARIABLE_COLOUR', + }, + ...Variables.jsonFlyoutCategoryBlocks( + workspace, + workspace.getVariableMap().getAllVariables(), + false, + 'variables_get_dynamic', + 'variables_set_dynamic', + ), + ]; +} + /** * Construct the elements (blocks and button) required by the flyout for the * variable category. @@ -76,7 +163,7 @@ export const onCreateVariableButtonClick_Colour = colourButtonClickHandler; * @param workspace The workspace containing variables. * @returns Array of XML elements. */ -export function flyoutCategory(workspace: WorkspaceSvg): Element[] { +function xmlFlyoutCategory(workspace: WorkspaceSvg): Element[] { let xmlList = new Array(); let button = document.createElement('button'); button.setAttribute('text', Msg['NEW_STRING_VARIABLE']); @@ -116,7 +203,7 @@ export function flyoutCategory(workspace: WorkspaceSvg): Element[] { * @returns Array of XML block elements. */ export function flyoutCategoryBlocks(workspace: Workspace): Element[] { - const variableModelList = workspace.getAllVariables(); + const variableModelList = workspace.getVariableMap().getAllVariables(); const xmlList = []; if (variableModelList.length > 0) { @@ -129,7 +216,7 @@ export function flyoutCategoryBlocks(workspace: Workspace): Element[] { xmlList.push(block); } if (Blocks['variables_get_dynamic']) { - variableModelList.sort(VariableModel.compareByName); + variableModelList.sort(Variables.compareByName); for (let i = 0, variable; (variable = variableModelList[i]); i++) { const block = xml.createElement('block'); block.setAttribute('type', 'variables_get_dynamic'); diff --git a/core/widgetdiv.ts b/packages/blockly/core/widgetdiv.ts similarity index 82% rename from core/widgetdiv.ts rename to packages/blockly/core/widgetdiv.ts index 897698611e0..d07f7fb502b 100644 --- a/core/widgetdiv.ts +++ b/packages/blockly/core/widgetdiv.ts @@ -6,8 +6,10 @@ // Former goog.module ID: Blockly.WidgetDiv +import * as browserEvents from './browser_events.js'; import * as common from './common.js'; import {Field} from './field.js'; +import {ReturnEphemeralFocus, getFocusManager} from './focus_manager.js'; import * as dom from './utils/dom.js'; import type {Rect} from './utils/rect.js'; import type {Size} from './utils/size.js'; @@ -34,6 +36,9 @@ let themeClassName = ''; /** The HTML container for popup overlays (e.g. editor widgets). */ let containerDiv: HTMLDivElement | null; +/** Callback to FocusManager to return ephemeral focus when the div closes. */ +let returnEphemeralFocus: ReturnEphemeralFocus | null = null; + /** * Returns the HTML container for editor widgets. * @@ -62,14 +67,23 @@ export function testOnly_setDiv(newDiv: HTMLDivElement | null) { export function createDom() { const container = common.getParentContainer() || document.body; - if (document.querySelector('.' + containerClassName)) { - containerDiv = document.querySelector('.' + containerClassName); + const existingContainer = document.querySelector('div.' + containerClassName); + if (existingContainer) { + containerDiv = existingContainer as HTMLDivElement; } else { - containerDiv = document.createElement('div') as HTMLDivElement; + containerDiv = document.createElement('div'); containerDiv.className = containerClassName; + containerDiv.tabIndex = -1; } - container.appendChild(containerDiv!); + browserEvents.conditionalBind( + containerDiv, + 'keydown', + null, + common.globalShortcutHandler, + ); + + container.appendChild(containerDiv); } /** @@ -80,12 +94,18 @@ export function createDom() { * @param newDispose Optional cleanup function to be run when the widget is * closed. * @param workspace The workspace associated with the widget owner. + * @param manageEphemeralFocus Whether ephemeral focus should be managed + * according to the widget div's lifetime. Note that if a false value is + * passed in here then callers should manage ephemeral focus directly + * otherwise focus may not properly restore when the widget closes. Defaults + * to true. */ export function show( newOwner: unknown, rtl: boolean, newDispose: () => void, workspace?: WorkspaceSvg | null, + manageEphemeralFocus: boolean = true, ) { hide(); owner = newOwner; @@ -110,6 +130,9 @@ export function show( if (themeClassName) { dom.addClass(div, themeClassName); } + if (manageEphemeralFocus) { + returnEphemeralFocus = getFocusManager().takeEphemeralFocus(div); + } } /** @@ -126,8 +149,10 @@ export function hide() { div.style.display = 'none'; div.style.left = ''; div.style.top = ''; - if (dispose) dispose(); - dispose = null; + if (dispose) { + dispose(); + dispose = null; + } div.textContent = ''; if (rendererClassName) { @@ -139,6 +164,11 @@ export function hide() { themeClassName = ''; } (common.getMainWorkspace() as WorkspaceSvg).markFocused(); + + if (returnEphemeralFocus) { + returnEphemeralFocus(); + returnEphemeralFocus = null; + } } /** @@ -166,10 +196,22 @@ export function hideIfOwner(oldOwner: unknown) { * Destroy the widget and hide the div if it is being used by an object in the * specified workspace, or if it is used by an unknown workspace. * - * @param oldOwnerWorkspace The workspace that was using this container. + * @param workspace The workspace that was using this container. */ -export function hideIfOwnerIsInWorkspace(oldOwnerWorkspace: WorkspaceSvg) { - if (ownerWorkspace === null || ownerWorkspace === oldOwnerWorkspace) { +export function hideIfOwnerIsInWorkspace(workspace: WorkspaceSvg) { + let ownerIsInWorkspace = ownerWorkspace === null; + // Check if the given workspace is a parent workspace of the one containing + // our owner. + let currentWorkspace: WorkspaceSvg | null = workspace; + while (!ownerIsInWorkspace && currentWorkspace) { + if (currentWorkspace === workspace) { + ownerIsInWorkspace = true; + break; + } + currentWorkspace = workspace.options.parentWorkspace; + } + + if (ownerIsInWorkspace) { hide(); } } diff --git a/core/workspace.ts b/packages/blockly/core/workspace.ts similarity index 78% rename from core/workspace.ts rename to packages/blockly/core/workspace.ts index 89c79723726..88745f4205c 100644 --- a/core/workspace.ts +++ b/packages/blockly/core/workspace.ts @@ -21,24 +21,30 @@ import * as common from './common.js'; import type {ConnectionDB} from './connection_db.js'; import type {Abstract} from './events/events_abstract.js'; import * as eventUtils from './events/utils.js'; -import type {IASTNodeLocation} from './interfaces/i_ast_node_location.js'; +import type {IBoundedElement} from './interfaces/i_bounded_element.js'; import type {IConnectionChecker} from './interfaces/i_connection_checker.js'; import {IProcedureMap} from './interfaces/i_procedure_map.js'; +import type {IVariableMap} from './interfaces/i_variable_map.js'; +import type { + IVariableModel, + IVariableState, +} from './interfaces/i_variable_model.js'; import {ObservableProcedureMap} from './observable_procedure_map.js'; import {Options} from './options.js'; import * as registry from './registry.js'; import * as arrayUtils from './utils/array.js'; +import * as deprecation from './utils/deprecation.js'; import * as idGenerator from './utils/idgenerator.js'; import * as math from './utils/math.js'; +import {Rect} from './utils/rect.js'; import type * as toolbox from './utils/toolbox.js'; -import {VariableMap} from './variable_map.js'; -import type {VariableModel} from './variable_model.js'; +import {deleteVariable, getVariableUsesById} from './variables.js'; /** * Class for a workspace. This is a data structure that contains blocks. * There is no UI, and can be created headlessly. */ -export class Workspace implements IASTNodeLocation { +export class Workspace { /** * Angle away from the horizontal to sweep for blocks. Order of execution is * generally top to bottom, but a small angle changes the scan to give a bit @@ -107,8 +113,9 @@ export class Workspace implements IASTNodeLocation { protected redoStack_: Abstract[] = []; private readonly blockDB = new Map(); private readonly typedBlocksDB = new Map(); - private variableMap: VariableMap; + private variableMap: IVariableMap>; private procedureMap: IProcedureMap = new ObservableProcedureMap(); + private readOnly = false; /** * Blocks in the flyout can refer to variables that don't exist in the main @@ -118,7 +125,9 @@ export class Workspace implements IASTNodeLocation { * these by tracking "potential" variables in the flyout. These variables * become real when references to them are dragged into the main workspace. */ - private potentialVariableMap: VariableMap | null = null; + private potentialVariableMap: IVariableMap< + IVariableModel + > | null = null; /** @param opt_options Dictionary of options. */ constructor(opt_options?: Options) { @@ -144,7 +153,10 @@ export class Workspace implements IASTNodeLocation { * all of the named variables in the workspace, including variables that are * not currently in use. */ + const VariableMap = this.getVariableMapClass(); this.variableMap = new VariableMap(this); + + this.setIsReadOnly(this.options.readOnly); } /** @@ -171,10 +183,31 @@ export class Workspace implements IASTNodeLocation { a: Block | WorkspaceComment, b: Block | WorkspaceComment, ): number { + const wrap = (element: Block | WorkspaceComment) => { + return { + getBoundingRectangle: () => { + const xy = element.getRelativeToSurfaceXY(); + return new Rect(xy.y, xy.y, xy.x, xy.x); + }, + moveBy: () => {}, + }; + }; + return this.sortByOrigin(wrap(a), wrap(b)); + } + + /** + * Sorts bounded elements on the workspace by their relative position, top to + * bottom (with slight LTR or RTL bias). + * + * @param a The first element to sort. + * @param b The second elment to sort. + * @returns -1, 0 or 1 depending on the sort order. + */ + protected sortByOrigin(a: IBoundedElement, b: IBoundedElement): number { const offset = Math.sin(math.toRadians(Workspace.SCAN_ANGLE)) * (this.RTL ? -1 : 1); - const aXY = a.getRelativeToSurfaceXY(); - const bXY = b.getRelativeToSurfaceXY(); + const aXY = a.getBoundingRectangle().getOrigin(); + const bXY = b.getBoundingRectangle().getOrigin(); return aXY.y + offset * aXY.x - (bXY.y + offset * bXY.x); } @@ -357,7 +390,14 @@ export class Workspace implements IASTNodeLocation { this.topComments[this.topComments.length - 1].dispose(); } eventUtils.setGroup(existingGroup); - this.variableMap.clear(); + // If this is a flyout workspace, its variable map is shared with the + // parent workspace, so we either don't want to disturb it if we're just + // disposing the flyout, or if the flyout is being disposed because the + // main workspace is being disposed, then the main workspace will handle + // cleaning it up. + if (!this.isFlyout) { + this.variableMap.clear(); + } if (this.potentialVariableMap) { this.potentialVariableMap.clear(); } @@ -368,19 +408,29 @@ export class Workspace implements IASTNodeLocation { /* Begin functions that are just pass-throughs to the variable map. */ /** - * Rename a variable by updating its name in the variable map. Identify the - * variable to rename with the given ID. + * Rename a variable by updating its name in the variable + * map. Identify the variable to rename with the given ID. * + * @deprecated v12: use Blockly.Workspace.getVariableMap().renameVariable * @param id ID of the variable to rename. * @param newName New variable name. */ renameVariableById(id: string, newName: string) { - this.variableMap.renameVariableById(id, newName); + deprecation.warn( + 'Blockly.Workspace.renameVariableById', + 'v12', + 'v13', + 'Blockly.Workspace.getVariableMap().renameVariable', + ); + const variable = this.variableMap.getVariableById(id); + if (!variable) return; + this.variableMap.renameVariable(variable, newName); } /** * Create a variable with a given name, optional type, and optional ID. * + * @deprecated v12: use Blockly.Workspace.getVariableMap().createVariable. * @param name The name of the variable. This must be unique across variables * and procedures. * @param opt_type The type of the variable like 'int' or 'string'. @@ -393,40 +443,79 @@ export class Workspace implements IASTNodeLocation { name: string, opt_type?: string | null, opt_id?: string | null, - ): VariableModel { - return this.variableMap.createVariable(name, opt_type, opt_id); + ): IVariableModel { + deprecation.warn( + 'Blockly.Workspace.createVariable', + 'v12', + 'v13', + 'Blockly.Workspace.getVariableMap().createVariable', + ); + return this.variableMap.createVariable( + name, + opt_type ?? undefined, + opt_id ?? undefined, + ); } /** * Find all the uses of the given variable, which is identified by ID. * + * @deprecated v12: use Blockly.Workspace.getVariableMap().getVariableUsesById * @param id ID of the variable to find. * @returns Array of block usages. */ getVariableUsesById(id: string): Block[] { - return this.variableMap.getVariableUsesById(id); + deprecation.warn( + 'Blockly.Workspace.getVariableUsesById', + 'v12', + 'v13', + 'Blockly.Variables.getVariableUsesById', + ); + return getVariableUsesById(this, id); } /** * Delete a variables by the passed in ID and all of its uses from this * workspace. May prompt the user for confirmation. * + * @deprecated v12: use Blockly.Workspace.getVariableMap().deleteVariable. * @param id ID of variable to delete. */ deleteVariableById(id: string) { - this.variableMap.deleteVariableById(id); + deprecation.warn( + 'Blockly.Workspace.deleteVariableById', + 'v12', + 'v13', + 'Blockly.Workspace.getVariableMap().deleteVariable', + ); + const variable = this.variableMap.getVariableById(id); + if (!variable) { + console.warn(`Can't delete non-existent variable: ${id}`); + return; + } + deleteVariable(this, variable); } /** * Find the variable by the given name and return it. Return null if not * found. * + * @deprecated v12: use Blockly.Workspace.getVariableMap().getVariable. * @param name The name to check for. * @param opt_type The type of the variable. If not provided it defaults to * the empty string, which is a specific type. * @returns The variable with the given name. */ - getVariable(name: string, opt_type?: string): VariableModel | null { + getVariable( + name: string, + opt_type?: string, + ): IVariableModel | null { + deprecation.warn( + 'Blockly.Workspace.getVariable', + 'v12', + 'v13', + 'Blockly.Workspace.getVariableMap().getVariable', + ); // TODO (#1559): Possibly delete this function after resolving #1559. return this.variableMap.getVariable(name, opt_type); } @@ -434,10 +523,17 @@ export class Workspace implements IASTNodeLocation { /** * Find the variable by the given ID and return it. Return null if not found. * + * @deprecated v12: use Blockly.Workspace.getVariableMap().getVariableById. * @param id The ID to check for. * @returns The variable with the given ID. */ - getVariableById(id: string): VariableModel | null { + getVariableById(id: string): IVariableModel | null { + deprecation.warn( + 'Blockly.Workspace.getVariableById', + 'v12', + 'v13', + 'Blockly.Workspace.getVariableMap().getVariableById', + ); return this.variableMap.getVariableById(id); } @@ -445,40 +541,51 @@ export class Workspace implements IASTNodeLocation { * Find the variable with the specified type. If type is null, return list of * variables with empty string type. * + * @deprecated v12: use Blockly.Workspace.getVariableMap().getVariablesOfType. * @param type Type of the variables to find. * @returns The sought after variables of the passed in type. An empty array * if none are found. */ - getVariablesOfType(type: string | null): VariableModel[] { - return this.variableMap.getVariablesOfType(type); - } - - /** - * Return all variable types. - * - * @returns List of variable types. - * @internal - */ - getVariableTypes(): string[] { - return this.variableMap.getVariableTypes(this); + getVariablesOfType(type: string | null): IVariableModel[] { + deprecation.warn( + 'Blockly.Workspace.getVariablesOfType', + 'v12', + 'v13', + 'Blockly.Workspace.getVariableMap().getVariablesOfType', + ); + return this.variableMap.getVariablesOfType(type ?? ''); } /** * Return all variables of all types. * + * @deprecated v12: use Blockly.Workspace.getVariableMap().getAllVariables. * @returns List of variable models. */ - getAllVariables(): VariableModel[] { + getAllVariables(): IVariableModel[] { + deprecation.warn( + 'Blockly.Workspace.getAllVariables', + 'v12', + 'v13', + 'Blockly.Workspace.getVariableMap().getAllVariables', + ); return this.variableMap.getAllVariables(); } /** * Returns all variable names of all types. * + * @deprecated v12: use Blockly.Workspace.getVariableMap().getAllVariables. * @returns List of all variable names of all types. */ getAllVariableNames(): string[] { - return this.variableMap.getAllVariableNames(); + deprecation.warn( + 'Blockly.Workspace.getAllVariableNames', + 'v12', + 'v13', + 'Blockly.Workspace.getVariableMap().getAllVariables', + ); + return this.variableMap.getAllVariables().map((v) => v.getName()); } /* End functions that are just pass-throughs to the variable map. */ /** @@ -770,9 +877,10 @@ export class Workspace implements IASTNodeLocation { * These exist in the flyout but not in the workspace. * * @returns The potential variable map. - * @internal */ - getPotentialVariableMap(): VariableMap | null { + getPotentialVariableMap(): IVariableMap< + IVariableModel + > | null { return this.potentialVariableMap; } @@ -782,7 +890,8 @@ export class Workspace implements IASTNodeLocation { * @internal */ createPotentialVariableMap() { - this.potentialVariableMap = new VariableMap(this); + const VariableMap = this.getVariableMapClass(); + this.potentialVariableMap = new VariableMap(this, true); } /** @@ -790,7 +899,7 @@ export class Workspace implements IASTNodeLocation { * * @returns The variable map. */ - getVariableMap(): VariableMap { + getVariableMap(): IVariableMap> { return this.variableMap; } @@ -800,7 +909,7 @@ export class Workspace implements IASTNodeLocation { * @param variableMap The variable map. * @internal */ - setVariableMap(variableMap: VariableMap) { + setVariableMap(variableMap: IVariableMap>) { this.variableMap = variableMap; } @@ -849,4 +958,37 @@ export class Workspace implements IASTNodeLocation { static getAll(): Workspace[] { return common.getAllWorkspaces(); } + + protected getVariableMapClass(): new ( + ...p1: any[] + ) => IVariableMap> { + const VariableMap = registry.getClassFromOptions( + registry.Type.VARIABLE_MAP, + this.options, + true, + ); + if (!VariableMap) { + throw new Error('No variable map is registered.'); + } + return VariableMap; + } + + /** + * Returns whether or not this workspace is in readonly mode. + * + * @returns True if the workspace is readonly, otherwise false. + */ + isReadOnly(): boolean { + return this.readOnly; + } + + /** + * Sets whether or not this workspace is in readonly mode. + * + * @param readOnly True to make the workspace readonly, otherwise false. + */ + setIsReadOnly(readOnly: boolean) { + this.readOnly = readOnly; + this.options.readOnly = readOnly; + } } diff --git a/packages/blockly/core/workspace_audio.ts b/packages/blockly/core/workspace_audio.ts new file mode 100644 index 00000000000..46f18ce6108 --- /dev/null +++ b/packages/blockly/core/workspace_audio.ts @@ -0,0 +1,137 @@ +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Object in charge of loading, storing, and playing audio for a + * workspace. + * + * @class + */ +// Former goog.module ID: Blockly.WorkspaceAudio + +import type {WorkspaceSvg} from './workspace_svg.js'; + +/** + * Prevent a sound from playing if another sound preceded it within this many + * milliseconds. + */ +const SOUND_LIMIT = 100; + +/** + * Class for loading, storing, and playing audio for a workspace. + */ +export class WorkspaceAudio { + /** Database of pre-loaded sounds. */ + private sounds = new Map(); + + /** Time that the last sound was played. */ + private lastSound: Date | null = null; + + /** Whether the audio is muted or not. */ + private muted: boolean = false; + + /** Audio context used for playback. */ + private readonly context?: AudioContext; + + /** + * @param parentWorkspace The parent of the workspace this audio object + * belongs to, or null. + */ + constructor(private parentWorkspace: WorkspaceSvg) { + if (window.AudioContext) { + this.context = new AudioContext(); + } + } + + /** + * Dispose of this audio manager. + * + * @internal + */ + dispose() { + this.sounds.clear(); + this.context?.close(); + } + + /** + * Load an audio file. Cache it, ready for instantaneous playing. + * + * @param filenames Single-item array containing the URL for the sound file. + * Any items after the first item are ignored. + * @param name Name of sound. + */ + async load(filenames: string[], name: string) { + if (!filenames.length) { + return; + } + + const response = await fetch(filenames[0]); + const arrayBuffer = await response.arrayBuffer(); + this.context?.decodeAudioData(arrayBuffer, (audioBuffer) => { + this.sounds.set(name, audioBuffer); + }); + } + + /** + * Play a named sound at specified volume. If volume is not specified, + * use full volume (1). + * + * @param name Name of sound. + * @param opt_volume Volume of sound (0-1). + */ + async play(name: string, opt_volume?: number) { + if (this.muted || opt_volume === 0 || !this.context) { + return; + } + const sound = this.sounds.get(name); + if (sound) { + // Don't play one sound on top of another. + const now = new Date(); + if ( + this.lastSound !== null && + now.getTime() - this.lastSound.getTime() < SOUND_LIMIT + ) { + return; + } + this.lastSound = now; + + if (this.context.state === 'suspended') { + await this.context.resume(); + } + + const source = this.context.createBufferSource(); + const gainNode = this.context.createGain(); + gainNode.gain.value = opt_volume ?? 1; + gainNode.connect(this.context.destination); + source.buffer = sound; + source.connect(gainNode); + + source.addEventListener('ended', () => { + source.disconnect(); + gainNode.disconnect(); + }); + + source.start(); + } else if (this.parentWorkspace) { + // Maybe a workspace on a lower level knows about this sound. + this.parentWorkspace.getAudioManager().play(name, opt_volume); + } + } + + /** + * @param muted If true, mute sounds. Otherwise, play them. + */ + setMuted(muted: boolean) { + this.muted = muted; + } + + /** + * @returns Whether the audio is currently muted or not. + */ + getMuted(): boolean { + return this.muted; + } +} diff --git a/core/workspace_dragger.ts b/packages/blockly/core/workspace_dragger.ts similarity index 86% rename from core/workspace_dragger.ts rename to packages/blockly/core/workspace_dragger.ts index 7ad5651f791..89da600dc01 100644 --- a/core/workspace_dragger.ts +++ b/packages/blockly/core/workspace_dragger.ts @@ -11,7 +11,6 @@ */ // Former goog.module ID: Blockly.WorkspaceDragger -import * as common from './common.js'; import {Coordinate} from './utils/coordinate.js'; import type {WorkspaceSvg} from './workspace_svg.js'; @@ -40,27 +39,12 @@ export class WorkspaceDragger { this.startScrollXY_ = new Coordinate(workspace.scrollX, workspace.scrollY); } - /** - * Sever all links from this object. - * - * @internal - */ - dispose() { - // AnyDuringMigration because: Type 'null' is not assignable to type - // 'WorkspaceSvg'. - this.workspace = null as AnyDuringMigration; - } - /** * Start dragging the workspace. * * @internal */ - startDrag() { - if (common.getSelected()) { - common.getSelected()!.unselect(); - } - } + startDrag() {} /** * Finish dragging the workspace and put everything back where it belongs. diff --git a/core/workspace_svg.ts b/packages/blockly/core/workspace_svg.ts similarity index 76% rename from core/workspace_svg.ts rename to packages/blockly/core/workspace_svg.ts index 6acd31c9c7f..c693225970f 100644 --- a/core/workspace_svg.ts +++ b/packages/blockly/core/workspace_svg.ts @@ -22,6 +22,10 @@ import type {Block} from './block.js'; import type {BlockSvg} from './block_svg.js'; import type {BlocklyOptions} from './blockly_options.js'; import * as browserEvents from './browser_events.js'; +import {TextInputBubble} from './bubbles/textinput_bubble.js'; +import {COMMENT_COLLAPSE_BAR_BUTTON_FOCUS_IDENTIFIER} from './comments/collapse_comment_bar_button.js'; +import {COMMENT_EDITOR_FOCUS_IDENTIFIER} from './comments/comment_editor.js'; +import {COMMENT_DELETE_BAR_BUTTON_FOCUS_IDENTIFIER} from './comments/delete_comment_bar_button.js'; import {RenderedWorkspaceComment} from './comments/rendered_workspace_comment.js'; import {WorkspaceComment} from './comments/workspace_comment.js'; import * as common from './common.js'; @@ -33,25 +37,37 @@ import { ContextMenuRegistry, } from './contextmenu_registry.js'; import * as dropDownDiv from './dropdowndiv.js'; +import {Abstract as AbstractEvent} from './events/events.js'; import {EventType} from './events/type.js'; import * as eventUtils from './events/utils.js'; +import {Flyout} from './flyout_base.js'; import type {FlyoutButton} from './flyout_button.js'; +import {getFocusManager} from './focus_manager.js'; import {Gesture} from './gesture.js'; import {Grid} from './grid.js'; -import type {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js'; +import {MutatorIcon} from './icons/mutator_icon.js'; +import {isAutoHideable} from './interfaces/i_autohideable.js'; import type {IBoundedElement} from './interfaces/i_bounded_element.js'; +import {IContextMenu} from './interfaces/i_contextmenu.js'; import type {IDragTarget} from './interfaces/i_drag_target.js'; import type {IFlyout} from './interfaces/i_flyout.js'; +import { + isFocusableNode, + type IFocusableNode, +} from './interfaces/i_focusable_node.js'; +import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; +import {hasBubble} from './interfaces/i_has_bubble.js'; import type {IMetricsManager} from './interfaces/i_metrics_manager.js'; import type {IToolbox} from './interfaces/i_toolbox.js'; -import type {Cursor} from './keyboard_nav/cursor.js'; +import type {LineCursor} from './keyboard_nav/line_cursor.js'; import type {Marker} from './keyboard_nav/marker.js'; import {LayerManager} from './layer_manager.js'; import {MarkerManager} from './marker_manager.js'; +import {Msg} from './msg.js'; +import {Navigator} from './navigator.js'; import {Options} from './options.js'; import * as Procedures from './procedures.js'; import * as registry from './registry.js'; -import * as renderManagement from './render_management.js'; import * as blockRendering from './renderers/common/block_rendering.js'; import type {Renderer} from './renderers/common/renderer.js'; import type {ScrollbarPair} from './scrollbar_pair.js'; @@ -60,6 +76,7 @@ import {Classic} from './theme/classic.js'; import {ThemeManager} from './theme_manager.js'; import * as Tooltip from './tooltip.js'; import type {Trashcan} from './trashcan.js'; +import * as aria from './utils/aria.js'; import * as arrayUtils from './utils/array.js'; import {Coordinate} from './utils/coordinate.js'; import * as dom from './utils/dom.js'; @@ -71,7 +88,6 @@ import {Svg} from './utils/svg.js'; import * as svgMath from './utils/svg_math.js'; import * as toolbox from './utils/toolbox.js'; import * as userAgent from './utils/useragent.js'; -import type {VariableModel} from './variable_model.js'; import * as Variables from './variables.js'; import * as VariablesDynamic from './variables_dynamic.js'; import * as WidgetDiv from './widgetdiv.js'; @@ -86,7 +102,10 @@ const ZOOM_TO_FIT_MARGIN = 20; * Class for a workspace. This is an onscreen area with optional trashcan, * scrollbars, bubbles, and dragging. */ -export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { +export class WorkspaceSvg + extends Workspace + implements IContextMenu, IFocusableNode, IFocusableTree +{ /** * A wrapper function called when a resize event occurs. * You can pass the result to `eventHandling.unbind`. @@ -222,7 +241,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { * The first parent div with 'injectionDiv' in the name, or null if not set. * Access this with getInjectionDiv. */ - private injectionDiv: Element | null = null; + private injectionDiv: HTMLElement | null = null; /** * Last known position of the page scroll. @@ -301,6 +320,9 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { /** True if keyboard accessibility mode is on, false otherwise. */ keyboardAccessibilityMode = false; + /** True iff a keyboard-initiated move ("drag") is in progress. */ + keyboardMoveInProgress = false; // TODO(#8960): Delete this. + /** The list of top-level bounded elements on the workspace. */ private topBoundedElements: IBoundedElement[] = []; @@ -319,6 +341,12 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { svgBubbleCanvas_!: SVGElement; zoomControls_: ZoomControls | null = null; + /** + * Navigator that handles moving focus between items in this workspace in + * response to keyboard navigation commands. + */ + private navigator = new Navigator(); + /** * @param options Dictionary of options. */ @@ -361,28 +389,31 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { /** Manager in charge of markers and cursors. */ this.markerManager = new MarkerManager(this); - if (Variables && Variables.flyoutCategory) { + if (Variables && Variables.internalFlyoutCategory) { this.registerToolboxCategoryCallback( Variables.CATEGORY_NAME, - Variables.flyoutCategory, + Variables.internalFlyoutCategory, ); } - if (VariablesDynamic && VariablesDynamic.flyoutCategory) { + if (VariablesDynamic && VariablesDynamic.internalFlyoutCategory) { this.registerToolboxCategoryCallback( VariablesDynamic.CATEGORY_NAME, - VariablesDynamic.flyoutCategory, + VariablesDynamic.internalFlyoutCategory, ); } - if (Procedures && Procedures.flyoutCategory) { + if (Procedures && Procedures.internalFlyoutCategory) { this.registerToolboxCategoryCallback( Procedures.CATEGORY_NAME, - Procedures.flyoutCategory, + Procedures.internalFlyoutCategory, ); this.addChangeListener(Procedures.mutatorOpenListener); } + // Set up callbacks to refresh the toolbox when variables change + this.addChangeListener(this.variableChangeCallback.bind(this)); + /** Object in charge of storing and updating the workspace theme. */ this.themeManager_ = this.options.parentWorkspace ? this.options.parentWorkspace.getThemeManager() @@ -441,28 +472,6 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { return this.componentManager; } - /** - * Add the cursor SVG to this workspaces SVG group. - * - * @param cursorSvg The SVG root of the cursor to be added to the workspace - * SVG group. - * @internal - */ - setCursorSvg(cursorSvg: SVGElement) { - this.markerManager.setCursorSvg(cursorSvg); - } - - /** - * Add the marker SVG to this workspaces SVG group. - * - * @param markerSvg The SVG root of the marker to be added to the workspace - * SVG group. - * @internal - */ - setMarkerSvg(markerSvg: SVGElement) { - this.markerManager.setMarkerSvg(markerSvg); - } - /** * Get the marker with the given ID. * @@ -472,10 +481,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { * @internal */ getMarker(id: string): Marker | null { - if (this.markerManager) { - return this.markerManager.getMarker(id); - } - return null; + return this.markerManager.getMarker(id); } /** @@ -483,11 +489,8 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { * * @returns The cursor for the workspace. */ - getCursor(): Cursor | null { - if (this.markerManager) { - return this.markerManager.getCursor(); - } - return null; + getCursor(): LineCursor { + return this.markerManager.getCursor(); } /** @@ -536,7 +539,12 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { */ refreshTheme() { if (this.svgGroup_) { - this.renderer.refreshDom(this.svgGroup_, this.getTheme()); + const isParentWorkspace = this.options.parentWorkspace === null; + this.renderer.refreshDom( + this.svgGroup_, + this.getTheme(), + isParentWorkspace ? this.getInjectionDiv() : undefined, + ); } // Update all blocks in workspace that have a style name. @@ -631,20 +639,24 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { // Before the SVG canvas, scale the coordinates. scale = this.scale; } + let ancestor: Element = element; do { // Loop through this block and every parent. - const xy = svgMath.getRelativeXY(element); - if (element === this.getCanvas() || element === this.getBubbleCanvas()) { + const xy = svgMath.getRelativeXY(ancestor); + if ( + ancestor === this.getCanvas() || + ancestor === this.getBubbleCanvas() + ) { // After the SVG canvas, don't scale the coordinates. scale = 1; } x += xy.x * scale; y += xy.y * scale; - element = element.parentNode as SVGElement; + ancestor = ancestor.parentNode as Element; } while ( - element && - element !== this.getParentSvg() && - element !== this.getInjectionDiv() + ancestor && + ancestor !== this.getParentSvg() && + ancestor !== this.getInjectionDiv() ); return new Coordinate(x, y); } @@ -682,7 +694,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { * @returns The first parent div with 'injectionDiv' in the name. * @internal */ - getInjectionDiv(): Element { + getInjectionDiv(): HTMLElement { // NB: it would be better to pass this in at createDom, but is more likely // to break existing uses of Blockly. if (!this.injectionDiv) { @@ -690,7 +702,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { while (element) { const classes = element.getAttribute('class') || ''; if ((' ' + classes + ' ').includes(' injectionDiv ')) { - this.injectionDiv = element; + this.injectionDiv = element as HTMLElement; break; } element = element.parentNode as Element; @@ -734,7 +746,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { * 'blocklyMutatorBackground'. * @returns The workspace's SVG group. */ - createDom(opt_backgroundClass?: string, injectionDiv?: Element): Element { + createDom(opt_backgroundClass?: string, injectionDiv?: HTMLElement): Element { if (!this.injectionDiv) { this.injectionDiv = injectionDiv ?? null; } @@ -747,7 +759,17 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { * * */ - this.svgGroup_ = dom.createSvgElement(Svg.G, {'class': 'blocklyWorkspace'}); + this.svgGroup_ = dom.createSvgElement(Svg.G, { + 'class': 'blocklyWorkspace', + 'id': this.id, + }); + if (injectionDiv) { + aria.setState( + this.svgGroup_, + aria.State.LABEL, + Msg['WORKSPACE_ARIA_LABEL'], + ); + } // Note that a alone does not receive mouse events--it must have a // valid target inside it. If no background class is specified, as in the @@ -760,8 +782,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { ); if (opt_backgroundClass === 'blocklyMainBackground' && this.grid) { - this.svgBackground_.style.fill = - 'url(#' + this.grid.getPatternId() + ')'; + this.svgBackground_.style.fill = 'var(--blocklyGridPattern)'; } else { this.themeManager_.subscribe( this.svgBackground_, @@ -788,12 +809,16 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { // which otherwise prevents zoom/scroll events from being observed in // Safari. Once that bug is fixed it should be removed. this.dummyWheelListener = () => {}; - document.body.addEventListener('wheel', this.dummyWheelListener); + document.body.addEventListener('wheel', this.dummyWheelListener, { + passive: true, + }); browserEvents.conditionalBind( this.svgGroup_, 'wheel', this, this.onMouseWheel, + false, + {passive: false}, ); } @@ -816,9 +841,18 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { this.options, ); - if (CursorClass) this.markerManager.setCursor(new CursorClass()); + if (CursorClass) this.markerManager.setCursor(new CursorClass(this)); + + const isParentWorkspace = this.options.parentWorkspace === null; + this.renderer.createDom( + this.svgGroup_, + this.getTheme(), + isParentWorkspace ? this.getInjectionDiv() : undefined, + ); + + // Only the top-level and flyout workspaces should be tabbable. + getFocusManager().registerTree(this, !!this.injectionDiv || this.isFlyout); - this.renderer.createDom(this.svgGroup_, this.getTheme()); return this.svgGroup_; } @@ -864,10 +898,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { } this.renderer.dispose(); - - if (this.markerManager) { - this.markerManager.dispose(); - } + this.markerManager.dispose(); super.dispose(); @@ -903,6 +934,10 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { document.body.removeEventListener('wheel', this.dummyWheelListener); this.dummyWheelListener = null; } + + if (getFocusManager().isRegistered(this)) { + getFocusManager().unregisterTree(this); + } } /** @@ -939,24 +974,36 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { } /** - * Add a flyout element in an element with the given tag name. + * Creates a new set of options from this workspace's options with just the + * values that are relevant to a flyout. * - * @param tagName What type of tag the flyout belongs in. - * @returns The element containing the flyout DOM. - * @internal + * @returns A subset of this workspace's options. */ - addFlyout(tagName: string | Svg | Svg): Element { - const workspaceOptions = new Options({ + copyOptionsForFlyout(): Options { + return new Options({ 'parentWorkspace': this, 'rtl': this.RTL, 'oneBasedIndex': this.options.oneBasedIndex, 'horizontalLayout': this.horizontalLayout, 'renderer': this.options.renderer, 'rendererOverrides': this.options.rendererOverrides, + 'plugins': this.options.plugins, + 'modalInputs': this.options.modalInputs, 'move': { 'scrollbars': true, }, } as BlocklyOptions); + } + + /** + * Add a flyout element in an element with the given tag name. + * + * @param tagName What type of tag the flyout belongs in. + * @returns The element containing the flyout DOM. + * @internal + */ + addFlyout(tagName: string | Svg | Svg): Element { + const workspaceOptions = this.copyOptionsForFlyout(); workspaceOptions.toolboxPosition = this.options.toolboxPosition; if (this.horizontalLayout) { const HorizontalFlyout = registry.getClassFromOptions( @@ -1045,8 +1092,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { resize() { if (this.toolbox) { this.toolbox.position(); - } - if (this.flyout) { + } else if (this.flyout) { this.flyout.position(); } @@ -1086,6 +1132,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { /** * @returns The layer manager for this workspace. + * @internal */ getLayerManager(): LayerManager | null { return this.layerManager; @@ -1263,10 +1310,6 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { .flatMap((block) => block.getDescendants(false)) .filter((block) => block.isInsertionMarker()) .forEach((block) => block.queueRender()); - - renderManagement - .finishQueuedRenders() - .then(() => void this.markerManager.updateMarkers()); } /** @@ -1302,59 +1345,32 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { } /** - * Refresh the toolbox unless there's a drag in progress. + * Handles any necessary updates when a variable changes. * * @internal */ - refreshToolboxSelection() { - const ws = this.isFlyout ? this.targetWorkspace : this; - if (ws && !ws.currentGesture_ && ws.toolbox && ws.toolbox.getFlyout()) { - ws.toolbox.refreshSelection(); + private variableChangeCallback(event: AbstractEvent) { + switch (event.type) { + case EventType.VAR_CREATE: + case EventType.VAR_DELETE: + case EventType.VAR_RENAME: + case EventType.VAR_TYPE_CHANGE: + this.refreshToolboxSelection(); + break; + default: } } /** - * Rename a variable by updating its name in the variable map. Update the - * flyout to show the renamed variable immediately. - * - * @param id ID of the variable to rename. - * @param newName New variable name. - */ - override renameVariableById(id: string, newName: string) { - super.renameVariableById(id, newName); - this.refreshToolboxSelection(); - } - - /** - * Delete a variable by the passed in ID. Update the flyout to show - * immediately that the variable is deleted. - * - * @param id ID of variable to delete. - */ - override deleteVariableById(id: string) { - super.deleteVariableById(id); - this.refreshToolboxSelection(); - } - - /** - * Create a new variable with the given name. Update the flyout to show the - * new variable immediately. + * Refresh the toolbox unless there's a drag in progress. * - * @param name The new variable's name. - * @param opt_type The type of the variable like 'int' or 'string'. - * Does not need to be unique. Field_variable can filter variables based - * on their type. This will default to '' which is a specific type. - * @param opt_id The unique ID of the variable. This will default to a UUID. - * @returns The newly created variable. + * @internal */ - override createVariable( - name: string, - opt_type?: string | null, - opt_id?: string | null, - ): VariableModel { - const newVar = super.createVariable(name, opt_type, opt_id); - this.refreshToolboxSelection(); - return newVar; + refreshToolboxSelection() { + const ws = this.isFlyout ? this.targetWorkspace : this; + if (ws && !ws.currentGesture_ && ws.toolbox && ws.toolbox.getFlyout()) { + ws.toolbox.refreshSelection(); + } } /** Make a list of all the delete areas for this workspace. */ @@ -1457,12 +1473,47 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { } /** - * Is the user currently dragging a block or scrolling the flyout/workspace? + * Indicate whether a keyboard move is in progress or not. + * + * Should be called with true when a keyboard move of an IDraggable + * is starts, and false when it finishes or is aborted. * - * @returns True if currently dragging or scrolling. + * N.B.: This method is experimental and internal-only. It is + * intended only to called only from the keyboard navigation plugin. + * Its signature and behaviour may be modified, or the method + * removed, at an time without notice and without being treated + * as a breaking change. + * + * TODO(#8960): Delete this. + * + * @internal + * @param inProgress Is a keyboard-initated move in progress? + */ + setKeyboardMoveInProgress(inProgress: boolean) { + this.keyboardMoveInProgress = inProgress; + } + + /** + * Returns true iff the user is currently engaged in a drag gesture, + * or if a keyboard-initated move is in progress. + * + * Dragging gestures normally entail moving a block or other item on + * the workspace, or scrolling the flyout/workspace. + * + * Keyboard-initated movements are implemnted using the dragging + * infrastructure and are intended to emulate (a subset of) drag + * gestures and so should typically be treated as if they were a + * gesture-based drag. + * + * @returns True iff a drag gesture or keyboard move is in porgress. */ isDragging(): boolean { - return this.currentGesture_ !== null && this.currentGesture_.isDragging(); + return ( + // TODO(#8960): Query Mover.isMoving to see if move is in + // progress rather than relying on a status flag. + this.keyboardMoveInProgress || + (this.currentGesture_ !== null && this.currentGesture_.isDragging()) + ); } /** @@ -1627,7 +1678,10 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { /** Clean up the workspace by ordering all the blocks in a column such that none overlap. */ cleanUp() { this.setResizesEnabled(false); - eventUtils.setGroup(true); + const existingGroup = eventUtils.getGroup(); + if (!existingGroup) { + eventUtils.setGroup(true); + } const topBlocks = this.getTopBlocks(true); const movableBlocks = topBlocks.filter((block) => block.isMovable()); @@ -1675,7 +1729,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { block.getHeightWidth().height + minBlockHeight; } - eventUtils.setGroup(false); + eventUtils.setGroup(existingGroup); this.setResizesEnabled(true); } @@ -1685,13 +1739,13 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { * @param e Mouse event. * @internal */ - showContextMenu(e: PointerEvent) { - if (this.options.readOnly || this.isFlyout) { + showContextMenu(e: Event) { + if (this.isReadOnly() || this.isFlyout) { return; } const menuOptions = ContextMenuRegistry.registry.getContextMenuOptions( - ContextMenuRegistry.ScopeType.WORKSPACE, - {workspace: this}, + {workspace: this, focusedNode: this}, + e, ); // Allow the developer to add or modify menuOptions. @@ -1699,7 +1753,15 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { this.configureContextMenu(menuOptions, e); } - ContextMenu.show(e, menuOptions, this.RTL, this); + let location; + if (e instanceof PointerEvent) { + location = new Coordinate(e.clientX, e.clientY); + } else { + // TODO: Get the location based on the workspace cursor location + location = svgMath.wsToScreenCoordinates(this, new Coordinate(5, 5)); + } + + ContextMenu.show(e, menuOptions, this.RTL, this, location); } /** @@ -2033,18 +2095,69 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { } /** - * Get the workspace's zoom factor. If the workspace has a parent, we call - * into the parent to get the workspace scale. + * Get the workspace's zoom factor. * * @returns The workspace zoom factor. Units: (pixels / workspaceUnit). */ getScale(): number { - if (this.options.parentWorkspace) { - return this.options.parentWorkspace.getScale(); - } return this.scale; } + /** + * Returns the absolute scale of the workspace. + * + * Workspace scaling is multiplicative; if a workspace B (e.g. a mutator editor) + * with scale Y is nested within a root workspace A with scale X, workspace B's + * effective scale is X * Y, because, as a child of A, it is already transformed + * by A's scaling factor, and then further transforms itself by its own scaling + * factor. Normally this Just Works, but for global elements (e.g. field + * editors) that are visually associated with a particular workspace but live at + * the top level of the DOM rather than being a child of their associated + * workspace, the absolute/effective scale may be needed to render + * appropriately. + * + * @returns The absolute/effective scale of the given workspace. + */ + getAbsoluteScale() { + // Returns a workspace's own scale, without regard to multiplicative scaling. + const getLocalScale = (workspace: WorkspaceSvg): number => { + // Workspaces in flyouts may have a distinct scale; use this if relevant. + if (workspace.isFlyout) { + const flyout = workspace.targetWorkspace?.getFlyout(); + if (flyout instanceof Flyout) { + return flyout.getFlyoutScale(); + } + } + + return workspace.getScale(); + }; + + const computeScale = (workspace: WorkspaceSvg, scale: number): number => { + // If the workspace has no parent, or it does have a parent but is not + // actually a child of its parent workspace in the DOM (this is the case for + // flyouts in the main workspace), we're done; just return the scale so far + // multiplied by the workspace's own scale. + if ( + !workspace.options.parentWorkspace || + !workspace.options.parentWorkspace + .getSvgGroup() + .contains(workspace.getSvgGroup()) + ) { + return scale * getLocalScale(workspace); + } + + // If there is a parent workspace, and this workspace is a child of it in + // the DOM, scales are multiplicative, so recurse up the workspace + // hierarchy. + return computeScale( + workspace.options.parentWorkspace, + scale * getLocalScale(workspace), + ); + }; + + return computeScale(this, 1); + } + /** * Scroll the workspace to a specified offset (in pixels), keeping in the * workspace bounds. See comment on workspaceSvg.scrollX for more detail on @@ -2154,8 +2267,8 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { * * @param comment comment to add. */ - override addTopComment(comment: WorkspaceComment) { - this.addTopBoundedElement(comment as RenderedWorkspaceComment); + override addTopComment(comment: RenderedWorkspaceComment) { + this.addTopBoundedElement(comment); super.addTopComment(comment); } @@ -2164,11 +2277,31 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { * * @param comment comment to remove. */ - override removeTopComment(comment: WorkspaceComment) { - this.removeTopBoundedElement(comment as RenderedWorkspaceComment); + override removeTopComment(comment: RenderedWorkspaceComment) { + this.removeTopBoundedElement(comment); super.removeTopComment(comment); } + /** + * Returns a list of comments on this workspace. + * + * @param ordered If true, sorts the comments based on their position. + * @returns A list of workspace comments. + */ + override getTopComments(ordered = false): RenderedWorkspaceComment[] { + return super.getTopComments(ordered) as RenderedWorkspaceComment[]; + } + + /** + * Returns the workspace comment with the given ID, if any. + * + * @param id The ID of the comment to retrieve. + * @returns The workspace comment with the given ID, or null. + */ + override getCommentById(id: string): RenderedWorkspaceComment | null { + return super.getCommentById(id) as RenderedWorkspaceComment | null; + } + override getRootWorkspace(): WorkspaceSvg | null { return super.getRootWorkspace() as WorkspaceSvg | null; } @@ -2196,8 +2329,15 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { * * @returns The top-level bounded elements. */ - getTopBoundedElements(): IBoundedElement[] { - return new Array().concat(this.topBoundedElements); + getTopBoundedElements(ordered = false): IBoundedElement[] { + const elements = new Array().concat( + this.topBoundedElements, + ); + if (ordered) { + elements.sort(this.sortByOrigin.bind(this)); + } + + return elements; } /** @@ -2310,36 +2450,47 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { /** * Look up the gesture that is tracking this touch stream on this workspace. - * May create a new gesture. + * + * Returns the gesture in progress, except: + * + * - If there is a keyboard-initiate move in progress then null will + * be returned - after calling event.preventDefault() and + * event.stopPropagation() to ensure the pointer event is ignored. + * - If there is a gesture in progress but event.type is + * 'pointerdown' then the in-progress gesture will be cancelled; + * this will result in null being returned. + * - If no gesutre is in progress but event is a pointerdown then a + * new gesture will be created and returned. * * @param e Pointer event. * @returns The gesture that is tracking this touch stream, or null if no * valid gesture exists. * @internal */ - getGesture(e: PointerEvent): Gesture | null { - const isStart = e.type === 'pointerdown'; - - const gesture = this.currentGesture_; - if (gesture) { - if (isStart && gesture.hasStarted()) { - console.warn('Tried to start the same gesture twice.'); - // That's funny. We must have missed a mouse up. - // Cancel it, rather than try to retrieve all of the state we need. - gesture.cancel(); - return null; - } - return gesture; + getGesture(e?: PointerEvent): Gesture | null { + // TODO(#8960): Query Mover.isMoving to see if move is in progress + // rather than relying on .keyboardMoveInProgress status flag. + if (this.keyboardMoveInProgress) { + // Normally these would be called from Gesture.doStart. + e?.preventDefault(); + e?.stopPropagation(); + return null; } - // No gesture existed on this workspace, but this looks like the start of a - // new gesture. - if (isStart) { + const isStart = e?.type === 'pointerdown'; + if (isStart && this.currentGesture_?.hasStarted()) { + console.warn('Tried to start the same gesture twice.'); + // That's funny. We must have missed a mouse up. + // Cancel it, rather than try to retrieve all of the state we need. + this.currentGesture_.cancel(); // Sets this.currentGesture_ to null. + } + if (!this.currentGesture_ && isStart) { + // No gesture existed on this workspace, but this looks like the + // start of a new gesture. this.currentGesture_ = new Gesture(e, this); - return this.currentGesture_; } - // No gesture existed and this event couldn't be the start of a new gesture. - return null; + + return this.currentGesture_; } /** @@ -2442,6 +2593,353 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { // We could call scroll here, but that has extra checks we don't need to do. this.translate(x, y); } + + /** + * Adds a CSS class to the workspace. + * + * @param className Name of class to add. + */ + addClass(className: string) { + if (this.injectionDiv) { + dom.addClass(this.injectionDiv, className); + } + } + + /** + * Removes a CSS class from the workspace. + * + * @param className Name of class to remove. + */ + removeClass(className: string) { + if (this.injectionDiv) { + dom.removeClass(this.injectionDiv, className); + } + } + + override setIsReadOnly(readOnly: boolean) { + super.setIsReadOnly(readOnly); + if (readOnly) { + this.addClass('blocklyReadOnly'); + } else { + this.removeClass('blocklyReadOnly'); + } + } + + /** + * Scrolls the provided bounds into view. + * + * In the case of small workspaces/large bounds, this function prioritizes + * getting the top left corner of the bounds into view. It also adds some + * padding around the bounds to allow the element to be comfortably in view. + * + * @internal + * @param bounds A rectangle to scroll into view, as best as possible. + * @param padding Amount of spacing to put between the bounds and the edge of + * the workspace's viewport. + */ + scrollBoundsIntoView(bounds: Rect, padding = 10) { + if (Gesture.inProgress()) { + // This can cause jumps during a drag and is only suited for keyboard nav. + return; + } + const scale = this.getScale(); + + const rawViewport = this.getMetricsManager().getViewMetrics(true); + const viewport = new Rect( + rawViewport.top, + rawViewport.top + rawViewport.height, + rawViewport.left, + rawViewport.left + rawViewport.width, + ); + + if ( + bounds.left >= viewport.left && + bounds.top >= viewport.top && + bounds.right <= viewport.right && + bounds.bottom <= viewport.bottom + ) { + // Do nothing if the block is fully inside the viewport. + return; + } + + // Add some padding to the bounds so the element is scrolled comfortably + // into view. + bounds = bounds.clone(); + bounds.top -= padding; + bounds.bottom += padding; + bounds.left -= padding; + bounds.right += padding; + + let deltaX = 0; + let deltaY = 0; + + if (bounds.left < viewport.left) { + deltaX = this.RTL + ? Math.min( + viewport.left - bounds.left, + viewport.right - bounds.right, // Don't move the right side out of view + ) + : viewport.left - bounds.left; + } else if (bounds.right > viewport.right) { + deltaX = this.RTL + ? viewport.right - bounds.right + : Math.max( + viewport.right - bounds.right, + viewport.left - bounds.left, // Don't move the left side out of view + ); + } + + if (bounds.top < viewport.top) { + deltaY = viewport.top - bounds.top; + } else if (bounds.bottom > viewport.bottom) { + deltaY = Math.max( + viewport.bottom - bounds.bottom, + viewport.top - bounds.top, // Don't move the top out of view + ); + } + + deltaX *= scale; + deltaY *= scale; + this.scroll(this.scrollX + deltaX, this.scrollY + deltaY); + } + + /** See IFocusableNode.getFocusableElement. */ + getFocusableElement(): HTMLElement | SVGElement { + return this.svgGroup_; + } + + /** See IFocusableNode.getFocusableTree. */ + getFocusableTree(): IFocusableTree { + return (this.isMutator && this.options.parentWorkspace) || this; + } + + /** See IFocusableNode.onNodeFocus. */ + onNodeFocus(): void {} + + /** See IFocusableNode.onNodeBlur. */ + onNodeBlur(): void {} + + /** See IFocusableNode.canBeFocused. */ + canBeFocused(): boolean { + return true; + } + + /** See IFocusableTree.getRootFocusableNode. */ + getRootFocusableNode(): IFocusableNode { + return this; + } + + /** See IFocusableTree.getRestoredFocusableNode. */ + getRestoredFocusableNode( + previousNode: IFocusableNode | null, + ): IFocusableNode | null { + if (!previousNode) { + const flyout = this.targetWorkspace?.getFlyout(); + if (this.isFlyout && flyout) { + // Return the first focusable item of the flyout. + return ( + flyout + .getContents() + .find((flyoutItem) => { + const element = flyoutItem.getElement(); + return isFocusableNode(element) && element.canBeFocused(); + }) + ?.getElement() ?? null + ); + } + return this.getTopBlocks(true)[0] ?? null; + } else return null; + } + + /** See IFocusableTree.getNestedTrees. */ + getNestedTrees(): Array { + const nestedWorkspaces = this.getAllBlocks() + .map((block) => block.getIcons()) + .flat() + .filter( + (icon): icon is MutatorIcon => + icon instanceof MutatorIcon && icon.bubbleIsVisible(), + ) + .map((icon) => icon.getBubble()?.getWorkspace()) + .filter((workspace) => !!workspace); + + const ownFlyout = this.getFlyout(true); + if (ownFlyout) { + nestedWorkspaces.push(ownFlyout.getWorkspace()); + } + + return nestedWorkspaces; + } + + /** + * Used for searching for a specific workspace comment. + * We can't use this.getWorkspaceCommentById because the workspace + * comment ids might not be globally unique, but the id assigned to + * the focusable element for the comment should be. + */ + private searchForWorkspaceComment( + id: string, + ): RenderedWorkspaceComment | undefined { + for (const comment of this.getTopComments()) { + if ( + comment instanceof RenderedWorkspaceComment && + comment.canBeFocused() && + comment.getFocusableElement().id === id + ) { + return comment; + } + } + } + + /** See IFocusableTree.lookUpFocusableNode. */ + lookUpFocusableNode(id: string): IFocusableNode | null { + // Check against flyout items if this workspace is part of a flyout. Note + // that blocks may match against this pass before reaching getBlockById() + // below (but only for a flyout workspace). + const flyout = this.targetWorkspace?.getFlyout(); + if (this.isFlyout && flyout) { + for (const flyoutItem of flyout.getContents()) { + const elem = flyoutItem.getElement(); + if ( + isFocusableNode(elem) && + elem.canBeFocused() && + elem.getFocusableElement().id === id + ) { + return elem; + } + } + } + + // Search for fields and connections (based on ID indicators). + const fieldIndicatorIndex = id.indexOf('_field_'); + const connectionIndicatorIndex = id.indexOf('_connection_'); + if (fieldIndicatorIndex !== -1) { + const blockId = id.substring(0, fieldIndicatorIndex); + const block = this.getBlockById(blockId); + if (block) { + for (const field of block.getFields()) { + if (field.canBeFocused() && field.getFocusableElement().id === id) { + return field; + } + } + } + return null; + } else if (connectionIndicatorIndex !== -1) { + const blockId = id.substring(0, connectionIndicatorIndex); + const block = this.getBlockById(blockId); + if (block) { + for (const connection of block.getConnections_(true)) { + if (connection.id === id) return connection; + } + } + return null; + } + + // Search for a specific workspace comment or comment icon if the ID + // indicates the presence of one. + const commentIdSeparatorIndex = Math.max( + id.indexOf(COMMENT_EDITOR_FOCUS_IDENTIFIER), + id.indexOf(COMMENT_COLLAPSE_BAR_BUTTON_FOCUS_IDENTIFIER), + id.indexOf(COMMENT_DELETE_BAR_BUTTON_FOCUS_IDENTIFIER), + ); + if (commentIdSeparatorIndex !== -1) { + const commentId = id.substring(0, commentIdSeparatorIndex); + const comment = this.searchForWorkspaceComment(commentId); + if (comment) { + if (id.indexOf(COMMENT_EDITOR_FOCUS_IDENTIFIER) > -1) { + return comment.getEditorFocusableNode(); + } else { + return ( + comment.view + .getCommentBarButtons() + .find((button) => button.getFocusableElement().id.includes(id)) ?? + null + ); + } + } + } + + // Search for a specific block. + // Don't use `getBlockById` because the block ID is not guaranteed + // to be globally unique, but the ID on the focusable element is. + const block = this.getAllBlocks(false).find( + (block) => block.getFocusableElement().id === id, + ); + if (block) return block; + + // Search for a workspace comment (semi-expensive). + const comment = this.searchForWorkspaceComment(id); + if (comment) { + return comment; + } + + // Search for icons and bubbles (which requires an expensive getAllBlocks). + const icons = this.getAllBlocks() + .map((block) => block.getIcons()) + .flat(); + for (const icon of icons) { + if (icon.canBeFocused() && icon.getFocusableElement().id === id) { + return icon; + } + if (hasBubble(icon)) { + const bubble = icon.getBubble(); + if ( + bubble && + bubble.canBeFocused() && + bubble.getFocusableElement().id === id + ) { + return bubble; + } else if ( + bubble instanceof TextInputBubble && + bubble.getEditor().getFocusableElement().id === id + ) { + return bubble.getEditor(); + } + } + } + + return null; + } + + /** See IFocusableTree.onTreeFocus. */ + onTreeFocus( + _node: IFocusableNode, + _previousTree: IFocusableTree | null, + ): void {} + + /** See IFocusableTree.onTreeBlur. */ + onTreeBlur(nextTree: IFocusableTree | null): void { + // If the flyout loses focus, make sure to close it unless focus is being + // lost to the toolbox or ephemeral focus. + if (this.isFlyout && this.targetWorkspace) { + // Only hide the flyout if the flyout's workspace is losing focus and that + // focus isn't returning to the flyout itself, the toolbox, or ephemeral. + if (getFocusManager().ephemeralFocusTaken()) return; + const toolbox = this.targetWorkspace.getToolbox(); + if (toolbox && nextTree === toolbox) return; + if (isAutoHideable(toolbox)) toolbox.autoHide(false); + } + } + + /** + * Returns an object responsible for coordinating movement of focus between + * items on this workspace in response to keyboard navigation commands. + * + * @returns This workspace's Navigator instance. + */ + getNavigator(): Navigator { + return this.navigator; + } + + /** + * Sets the Navigator instance used by this workspace. + * + * @param newNavigator A Navigator object to coordinate movement between + * elements on the workspace. + */ + setNavigator(newNavigator: Navigator) { + this.navigator = newNavigator; + } } /** diff --git a/core/xml.ts b/packages/blockly/core/xml.ts similarity index 95% rename from core/xml.ts rename to packages/blockly/core/xml.ts index cecc4dce20e..3c06a39cca9 100644 --- a/core/xml.ts +++ b/packages/blockly/core/xml.ts @@ -17,12 +17,15 @@ import * as eventUtils from './events/utils.js'; import type {Field} from './field.js'; import {IconType} from './icons/icon_types.js'; import {inputTypes} from './inputs/input_types.js'; +import type { + IVariableModel, + IVariableState, +} from './interfaces/i_variable_model.js'; import * as renderManagement from './render_management.js'; import {Coordinate} from './utils/coordinate.js'; import * as dom from './utils/dom.js'; import {Size} from './utils/size.js'; import * as utilsXml from './utils/xml.js'; -import type {VariableModel} from './variable_model.js'; import * as Variables from './variables.js'; import type {Workspace} from './workspace.js'; import {WorkspaceSvg} from './workspace_svg.js'; @@ -44,9 +47,7 @@ export function workspaceToDom(workspace: Workspace, skipId = false): Element { treeXml.appendChild(variablesElement); } for (const comment of workspace.getTopComments()) { - treeXml.appendChild( - saveWorkspaceComment(comment as AnyDuringMigration, skipId), - ); + treeXml.appendChild(saveWorkspaceComment(comment, skipId)); } const blocks = workspace.getTopBlocks(true); for (let i = 0; i < blocks.length; i++) { @@ -65,7 +66,7 @@ export function saveWorkspaceComment( if (!skipId) elem.setAttribute('id', comment.id); const workspace = comment.workspace; - const loc = comment.getRelativeToSurfaceXY(); + const loc = comment.getRelativeToSurfaceXY().clone(); loc.x = workspace.RTL ? workspace.getWidth() - loc.x : loc.x; elem.setAttribute('x', `${loc.x}`); elem.setAttribute('y', `${loc.y}`); @@ -87,14 +88,16 @@ export function saveWorkspaceComment( * @param variableList List of all variable models. * @returns Tree of XML elements. */ -export function variablesToDom(variableList: VariableModel[]): Element { +export function variablesToDom( + variableList: IVariableModel[], +): Element { const variables = utilsXml.createElement('variables'); for (let i = 0; i < variableList.length; i++) { const variable = variableList[i]; const element = utilsXml.createElement('variable'); - element.appendChild(utilsXml.createTextNode(variable.name)); - if (variable.type) { - element.setAttribute('type', variable.type); + element.appendChild(utilsXml.createTextNode(variable.getName())); + if (variable.getType()) { + element.setAttribute('type', variable.getType()); } element.id = variable.getId(); variables.appendChild(element); @@ -163,14 +166,10 @@ function fieldToDom(field: Field): Element | null { * @param element The XML element to which the field DOM should be attached. */ function allFieldsToDom(block: Block, element: Element) { - for (let i = 0; i < block.inputList.length; i++) { - const input = block.inputList[i]; - for (let j = 0; j < input.fieldRow.length; j++) { - const field = input.fieldRow[j]; - const fieldDom = fieldToDom(field); - if (fieldDom) { - element.appendChild(fieldDom); - } + for (const field of block.getFields()) { + const fieldDom = fieldToDom(field); + if (fieldDom) { + element.appendChild(fieldDom); } } } @@ -218,12 +217,24 @@ export function blockToDom( const comment = block.getIcon(IconType.COMMENT)!; const size = comment.getBubbleSize(); const pinned = comment.bubbleIsVisible(); + const location = comment.getBubbleLocation(); const commentElement = utilsXml.createElement('comment'); commentElement.appendChild(utilsXml.createTextNode(commentText)); commentElement.setAttribute('pinned', `${pinned}`); - commentElement.setAttribute('h', String(size.height)); - commentElement.setAttribute('w', String(size.width)); + commentElement.setAttribute('h', `${size.height}`); + commentElement.setAttribute('w', `${size.width}`); + if (location) { + commentElement.setAttribute( + 'x', + `${ + block.workspace.RTL + ? block.workspace.getWidth() - (location.x + size.width) + : location.x + }`, + ); + commentElement.setAttribute('y', `${location.y}`); + } element.appendChild(commentElement); } @@ -624,7 +635,7 @@ export function domToBlockInternal( ): Block { // Create top-level block. eventUtils.disable(); - const variablesBeforeCreation = workspace.getAllVariables(); + const variablesBeforeCreation = workspace.getVariableMap().getAllVariables(); let topBlock; try { topBlock = domToBlockHeadless(xmlBlock, workspace); @@ -690,7 +701,7 @@ export function domToVariables(xmlVariables: Element, workspace: Workspace) { const name = xmlChild.textContent; if (!name) return; - workspace.createVariable(name, type, id); + workspace.getVariableMap().createVariable(name, type ?? undefined, id); } } @@ -794,6 +805,8 @@ function applyCommentTagNodes(xmlChildren: Element[], block: Block) { const pinned = xmlChild.getAttribute('pinned') === 'true'; const width = parseInt(xmlChild.getAttribute('w') ?? '50', 10); const height = parseInt(xmlChild.getAttribute('h') ?? '50', 10); + let x = parseInt(xmlChild.getAttribute('x') ?? '', 10); + const y = parseInt(xmlChild.getAttribute('y') ?? '', 10); block.setCommentText(text); const comment = block.getIcon(IconType.COMMENT)!; @@ -802,8 +815,15 @@ function applyCommentTagNodes(xmlChildren: Element[], block: Block) { } // Set the pinned state of the bubble. comment.setBubbleVisible(pinned); + // Actually show the bubble after the block has been rendered. - setTimeout(() => comment.setBubbleVisible(pinned), 1); + setTimeout(() => { + if (!isNaN(x) && !isNaN(y)) { + x = block.workspace.RTL ? block.workspace.getWidth() - (x + width) : x; + comment.setBubbleLocation(new Coordinate(x, y)); + } + comment.setBubbleVisible(pinned); + }, 1); } } diff --git a/core/zoom_controls.ts b/packages/blockly/core/zoom_controls.ts similarity index 99% rename from core/zoom_controls.ts rename to packages/blockly/core/zoom_controls.ts index 4f14b73bed6..6bd1194231b 100644 --- a/core/zoom_controls.ts +++ b/packages/blockly/core/zoom_controls.ts @@ -373,8 +373,10 @@ export class ZoomControls implements IPositionable { * @param e A mouse down event. */ private zoom(amount: number, e: PointerEvent) { + this.workspace.beginCanvasTransition(); this.workspace.markFocused(); this.workspace.zoomCenter(amount); + setTimeout(this.workspace.endCanvasTransition.bind(this.workspace), 150); this.fireZoomEvent(); Touch.clearTouchIdentifier(); // Don't block future drags. e.stopPropagation(); // Don't start a workspace scroll. @@ -459,7 +461,7 @@ export class ZoomControls implements IPositionable { this.workspace.zoomCenter(amount); this.workspace.scrollCenter(); - setTimeout(this.workspace.endCanvasTransition.bind(this.workspace), 500); + setTimeout(this.workspace.endCanvasTransition.bind(this.workspace), 150); this.fireZoomEvent(); Touch.clearTouchIdentifier(); // Don't block future drags. e.stopPropagation(); // Don't start a workspace scroll. diff --git a/demos/blockfactory/analytics.js b/packages/blockly/demos/blockfactory/analytics.js similarity index 100% rename from demos/blockfactory/analytics.js rename to packages/blockly/demos/blockfactory/analytics.js diff --git a/demos/blockfactory/app_controller.js b/packages/blockly/demos/blockfactory/app_controller.js similarity index 100% rename from demos/blockfactory/app_controller.js rename to packages/blockly/demos/blockfactory/app_controller.js diff --git a/demos/blockfactory/block_definition_extractor.js b/packages/blockly/demos/blockfactory/block_definition_extractor.js similarity index 100% rename from demos/blockfactory/block_definition_extractor.js rename to packages/blockly/demos/blockfactory/block_definition_extractor.js diff --git a/demos/blockfactory/block_exporter_controller.js b/packages/blockly/demos/blockfactory/block_exporter_controller.js similarity index 100% rename from demos/blockfactory/block_exporter_controller.js rename to packages/blockly/demos/blockfactory/block_exporter_controller.js diff --git a/demos/blockfactory/block_exporter_tools.js b/packages/blockly/demos/blockfactory/block_exporter_tools.js similarity index 100% rename from demos/blockfactory/block_exporter_tools.js rename to packages/blockly/demos/blockfactory/block_exporter_tools.js diff --git a/demos/blockfactory/block_exporter_view.js b/packages/blockly/demos/blockfactory/block_exporter_view.js similarity index 100% rename from demos/blockfactory/block_exporter_view.js rename to packages/blockly/demos/blockfactory/block_exporter_view.js diff --git a/demos/blockfactory/block_library_controller.js b/packages/blockly/demos/blockfactory/block_library_controller.js similarity index 100% rename from demos/blockfactory/block_library_controller.js rename to packages/blockly/demos/blockfactory/block_library_controller.js diff --git a/demos/blockfactory/block_library_storage.js b/packages/blockly/demos/blockfactory/block_library_storage.js similarity index 100% rename from demos/blockfactory/block_library_storage.js rename to packages/blockly/demos/blockfactory/block_library_storage.js diff --git a/demos/blockfactory/block_library_view.js b/packages/blockly/demos/blockfactory/block_library_view.js similarity index 100% rename from demos/blockfactory/block_library_view.js rename to packages/blockly/demos/blockfactory/block_library_view.js diff --git a/demos/blockfactory/block_option.js b/packages/blockly/demos/blockfactory/block_option.js similarity index 100% rename from demos/blockfactory/block_option.js rename to packages/blockly/demos/blockfactory/block_option.js diff --git a/demos/blockfactory/blocks.js b/packages/blockly/demos/blockfactory/blocks.js similarity index 100% rename from demos/blockfactory/blocks.js rename to packages/blockly/demos/blockfactory/blocks.js diff --git a/demos/blockfactory/cp.css b/packages/blockly/demos/blockfactory/cp.css similarity index 100% rename from demos/blockfactory/cp.css rename to packages/blockly/demos/blockfactory/cp.css diff --git a/demos/blockfactory/cp.js b/packages/blockly/demos/blockfactory/cp.js similarity index 100% rename from demos/blockfactory/cp.js rename to packages/blockly/demos/blockfactory/cp.js diff --git a/demos/blockfactory/factory.css b/packages/blockly/demos/blockfactory/factory.css similarity index 100% rename from demos/blockfactory/factory.css rename to packages/blockly/demos/blockfactory/factory.css diff --git a/demos/blockfactory/factory.js b/packages/blockly/demos/blockfactory/factory.js similarity index 100% rename from demos/blockfactory/factory.js rename to packages/blockly/demos/blockfactory/factory.js diff --git a/demos/blockfactory/factory_utils.js b/packages/blockly/demos/blockfactory/factory_utils.js similarity index 100% rename from demos/blockfactory/factory_utils.js rename to packages/blockly/demos/blockfactory/factory_utils.js diff --git a/demos/blockfactory/icon.png b/packages/blockly/demos/blockfactory/icon.png similarity index 100% rename from demos/blockfactory/icon.png rename to packages/blockly/demos/blockfactory/icon.png diff --git a/demos/blockfactory/index.html b/packages/blockly/demos/blockfactory/index.html similarity index 100% rename from demos/blockfactory/index.html rename to packages/blockly/demos/blockfactory/index.html diff --git a/demos/blockfactory/link.png b/packages/blockly/demos/blockfactory/link.png similarity index 100% rename from demos/blockfactory/link.png rename to packages/blockly/demos/blockfactory/link.png diff --git a/demos/blockfactory/standard_categories.js b/packages/blockly/demos/blockfactory/standard_categories.js similarity index 100% rename from demos/blockfactory/standard_categories.js rename to packages/blockly/demos/blockfactory/standard_categories.js diff --git a/demos/blockfactory/workspacefactory/wfactory_controller.js b/packages/blockly/demos/blockfactory/workspacefactory/wfactory_controller.js similarity index 100% rename from demos/blockfactory/workspacefactory/wfactory_controller.js rename to packages/blockly/demos/blockfactory/workspacefactory/wfactory_controller.js diff --git a/demos/blockfactory/workspacefactory/wfactory_generator.js b/packages/blockly/demos/blockfactory/workspacefactory/wfactory_generator.js similarity index 100% rename from demos/blockfactory/workspacefactory/wfactory_generator.js rename to packages/blockly/demos/blockfactory/workspacefactory/wfactory_generator.js diff --git a/demos/blockfactory/workspacefactory/wfactory_init.js b/packages/blockly/demos/blockfactory/workspacefactory/wfactory_init.js similarity index 100% rename from demos/blockfactory/workspacefactory/wfactory_init.js rename to packages/blockly/demos/blockfactory/workspacefactory/wfactory_init.js diff --git a/demos/blockfactory/workspacefactory/wfactory_model.js b/packages/blockly/demos/blockfactory/workspacefactory/wfactory_model.js similarity index 100% rename from demos/blockfactory/workspacefactory/wfactory_model.js rename to packages/blockly/demos/blockfactory/workspacefactory/wfactory_model.js diff --git a/demos/blockfactory/workspacefactory/wfactory_view.js b/packages/blockly/demos/blockfactory/workspacefactory/wfactory_view.js similarity index 100% rename from demos/blockfactory/workspacefactory/wfactory_view.js rename to packages/blockly/demos/blockfactory/workspacefactory/wfactory_view.js diff --git a/demos/code/code.js b/packages/blockly/demos/code/code.js similarity index 100% rename from demos/code/code.js rename to packages/blockly/demos/code/code.js diff --git a/demos/code/icon.png b/packages/blockly/demos/code/icon.png similarity index 100% rename from demos/code/icon.png rename to packages/blockly/demos/code/icon.png diff --git a/demos/code/icons.png b/packages/blockly/demos/code/icons.png similarity index 100% rename from demos/code/icons.png rename to packages/blockly/demos/code/icons.png diff --git a/demos/code/index.html b/packages/blockly/demos/code/index.html similarity index 100% rename from demos/code/index.html rename to packages/blockly/demos/code/index.html diff --git a/demos/code/msg/ar.js b/packages/blockly/demos/code/msg/ar.js similarity index 100% rename from demos/code/msg/ar.js rename to packages/blockly/demos/code/msg/ar.js diff --git a/demos/code/msg/be-tarask.js b/packages/blockly/demos/code/msg/be-tarask.js similarity index 100% rename from demos/code/msg/be-tarask.js rename to packages/blockly/demos/code/msg/be-tarask.js diff --git a/demos/code/msg/br.js b/packages/blockly/demos/code/msg/br.js similarity index 100% rename from demos/code/msg/br.js rename to packages/blockly/demos/code/msg/br.js diff --git a/demos/code/msg/ca.js b/packages/blockly/demos/code/msg/ca.js similarity index 100% rename from demos/code/msg/ca.js rename to packages/blockly/demos/code/msg/ca.js diff --git a/demos/code/msg/cs.js b/packages/blockly/demos/code/msg/cs.js similarity index 100% rename from demos/code/msg/cs.js rename to packages/blockly/demos/code/msg/cs.js diff --git a/demos/code/msg/da.js b/packages/blockly/demos/code/msg/da.js similarity index 100% rename from demos/code/msg/da.js rename to packages/blockly/demos/code/msg/da.js diff --git a/demos/code/msg/de.js b/packages/blockly/demos/code/msg/de.js similarity index 100% rename from demos/code/msg/de.js rename to packages/blockly/demos/code/msg/de.js diff --git a/demos/code/msg/el.js b/packages/blockly/demos/code/msg/el.js similarity index 100% rename from demos/code/msg/el.js rename to packages/blockly/demos/code/msg/el.js diff --git a/demos/code/msg/en.js b/packages/blockly/demos/code/msg/en.js similarity index 100% rename from demos/code/msg/en.js rename to packages/blockly/demos/code/msg/en.js diff --git a/demos/code/msg/es.js b/packages/blockly/demos/code/msg/es.js similarity index 100% rename from demos/code/msg/es.js rename to packages/blockly/demos/code/msg/es.js diff --git a/demos/code/msg/et.js b/packages/blockly/demos/code/msg/et.js similarity index 100% rename from demos/code/msg/et.js rename to packages/blockly/demos/code/msg/et.js diff --git a/demos/code/msg/fa.js b/packages/blockly/demos/code/msg/fa.js similarity index 100% rename from demos/code/msg/fa.js rename to packages/blockly/demos/code/msg/fa.js diff --git a/demos/code/msg/fr.js b/packages/blockly/demos/code/msg/fr.js similarity index 100% rename from demos/code/msg/fr.js rename to packages/blockly/demos/code/msg/fr.js diff --git a/demos/code/msg/he.js b/packages/blockly/demos/code/msg/he.js similarity index 100% rename from demos/code/msg/he.js rename to packages/blockly/demos/code/msg/he.js diff --git a/demos/code/msg/hr.js b/packages/blockly/demos/code/msg/hr.js similarity index 100% rename from demos/code/msg/hr.js rename to packages/blockly/demos/code/msg/hr.js diff --git a/demos/code/msg/hrx.js b/packages/blockly/demos/code/msg/hrx.js similarity index 100% rename from demos/code/msg/hrx.js rename to packages/blockly/demos/code/msg/hrx.js diff --git a/demos/code/msg/hu.js b/packages/blockly/demos/code/msg/hu.js similarity index 100% rename from demos/code/msg/hu.js rename to packages/blockly/demos/code/msg/hu.js diff --git a/demos/code/msg/ia.js b/packages/blockly/demos/code/msg/ia.js similarity index 100% rename from demos/code/msg/ia.js rename to packages/blockly/demos/code/msg/ia.js diff --git a/demos/code/msg/is.js b/packages/blockly/demos/code/msg/is.js similarity index 100% rename from demos/code/msg/is.js rename to packages/blockly/demos/code/msg/is.js diff --git a/demos/code/msg/it.js b/packages/blockly/demos/code/msg/it.js similarity index 100% rename from demos/code/msg/it.js rename to packages/blockly/demos/code/msg/it.js diff --git a/demos/code/msg/ja.js b/packages/blockly/demos/code/msg/ja.js similarity index 100% rename from demos/code/msg/ja.js rename to packages/blockly/demos/code/msg/ja.js diff --git a/demos/code/msg/kab.js b/packages/blockly/demos/code/msg/kab.js similarity index 100% rename from demos/code/msg/kab.js rename to packages/blockly/demos/code/msg/kab.js diff --git a/demos/code/msg/ko.js b/packages/blockly/demos/code/msg/ko.js similarity index 100% rename from demos/code/msg/ko.js rename to packages/blockly/demos/code/msg/ko.js diff --git a/demos/code/msg/mk.js b/packages/blockly/demos/code/msg/mk.js similarity index 100% rename from demos/code/msg/mk.js rename to packages/blockly/demos/code/msg/mk.js diff --git a/demos/code/msg/ms.js b/packages/blockly/demos/code/msg/ms.js similarity index 100% rename from demos/code/msg/ms.js rename to packages/blockly/demos/code/msg/ms.js diff --git a/demos/code/msg/nb.js b/packages/blockly/demos/code/msg/nb.js similarity index 100% rename from demos/code/msg/nb.js rename to packages/blockly/demos/code/msg/nb.js diff --git a/demos/code/msg/nl.js b/packages/blockly/demos/code/msg/nl.js similarity index 100% rename from demos/code/msg/nl.js rename to packages/blockly/demos/code/msg/nl.js diff --git a/demos/code/msg/oc.js b/packages/blockly/demos/code/msg/oc.js similarity index 100% rename from demos/code/msg/oc.js rename to packages/blockly/demos/code/msg/oc.js diff --git a/demos/code/msg/pl.js b/packages/blockly/demos/code/msg/pl.js similarity index 100% rename from demos/code/msg/pl.js rename to packages/blockly/demos/code/msg/pl.js diff --git a/demos/code/msg/pms.js b/packages/blockly/demos/code/msg/pms.js similarity index 100% rename from demos/code/msg/pms.js rename to packages/blockly/demos/code/msg/pms.js diff --git a/demos/code/msg/pt-br.js b/packages/blockly/demos/code/msg/pt-br.js similarity index 100% rename from demos/code/msg/pt-br.js rename to packages/blockly/demos/code/msg/pt-br.js diff --git a/demos/code/msg/ro.js b/packages/blockly/demos/code/msg/ro.js similarity index 100% rename from demos/code/msg/ro.js rename to packages/blockly/demos/code/msg/ro.js diff --git a/demos/code/msg/ru.js b/packages/blockly/demos/code/msg/ru.js similarity index 100% rename from demos/code/msg/ru.js rename to packages/blockly/demos/code/msg/ru.js diff --git a/demos/code/msg/sc.js b/packages/blockly/demos/code/msg/sc.js similarity index 100% rename from demos/code/msg/sc.js rename to packages/blockly/demos/code/msg/sc.js diff --git a/demos/code/msg/sk.js b/packages/blockly/demos/code/msg/sk.js similarity index 100% rename from demos/code/msg/sk.js rename to packages/blockly/demos/code/msg/sk.js diff --git a/demos/code/msg/sr.js b/packages/blockly/demos/code/msg/sr.js similarity index 100% rename from demos/code/msg/sr.js rename to packages/blockly/demos/code/msg/sr.js diff --git a/demos/code/msg/sv.js b/packages/blockly/demos/code/msg/sv.js similarity index 100% rename from demos/code/msg/sv.js rename to packages/blockly/demos/code/msg/sv.js diff --git a/demos/code/msg/ta.js b/packages/blockly/demos/code/msg/ta.js similarity index 100% rename from demos/code/msg/ta.js rename to packages/blockly/demos/code/msg/ta.js diff --git a/demos/code/msg/th.js b/packages/blockly/demos/code/msg/th.js similarity index 100% rename from demos/code/msg/th.js rename to packages/blockly/demos/code/msg/th.js diff --git a/demos/code/msg/tlh.js b/packages/blockly/demos/code/msg/tlh.js similarity index 100% rename from demos/code/msg/tlh.js rename to packages/blockly/demos/code/msg/tlh.js diff --git a/demos/code/msg/tr.js b/packages/blockly/demos/code/msg/tr.js similarity index 100% rename from demos/code/msg/tr.js rename to packages/blockly/demos/code/msg/tr.js diff --git a/demos/code/msg/uk.js b/packages/blockly/demos/code/msg/uk.js similarity index 100% rename from demos/code/msg/uk.js rename to packages/blockly/demos/code/msg/uk.js diff --git a/demos/code/msg/vi.js b/packages/blockly/demos/code/msg/vi.js similarity index 100% rename from demos/code/msg/vi.js rename to packages/blockly/demos/code/msg/vi.js diff --git a/demos/code/msg/zh-hans.js b/packages/blockly/demos/code/msg/zh-hans.js similarity index 100% rename from demos/code/msg/zh-hans.js rename to packages/blockly/demos/code/msg/zh-hans.js diff --git a/demos/code/msg/zh-hant.js b/packages/blockly/demos/code/msg/zh-hant.js similarity index 100% rename from demos/code/msg/zh-hant.js rename to packages/blockly/demos/code/msg/zh-hant.js diff --git a/demos/code/style.css b/packages/blockly/demos/code/style.css similarity index 100% rename from demos/code/style.css rename to packages/blockly/demos/code/style.css diff --git a/demos/index.html b/packages/blockly/demos/index.html similarity index 100% rename from demos/index.html rename to packages/blockly/demos/index.html diff --git a/demos/storage/icon.png b/packages/blockly/demos/storage/icon.png similarity index 100% rename from demos/storage/icon.png rename to packages/blockly/demos/storage/icon.png diff --git a/demos/storage/index.html b/packages/blockly/demos/storage/index.html similarity index 100% rename from demos/storage/index.html rename to packages/blockly/demos/storage/index.html diff --git a/eslint.config.mjs b/packages/blockly/eslint.config.mjs similarity index 96% rename from eslint.config.mjs rename to packages/blockly/eslint.config.mjs index 68f25133fa5..064c28a6477 100644 --- a/eslint.config.mjs +++ b/packages/blockly/eslint.config.mjs @@ -1,6 +1,7 @@ import eslint from '@eslint/js'; import googleStyle from 'eslint-config-google'; import jsdoc from 'eslint-plugin-jsdoc'; +import mochaPlugin from 'eslint-plugin-mocha'; import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; import globals from 'globals'; import tseslint from 'typescript-eslint'; @@ -59,7 +60,7 @@ function buildTSOverride({files, tsconfig}) { 'sourceType': 'module', parserOptions: { 'project': tsconfig, - 'tsconfigRootDir': '.', + 'tsconfigRootDir': process.cwd(), }, globals: { ...globals.browser, @@ -88,7 +89,8 @@ function buildTSOverride({files, tsconfig}) { '@typescript-eslint/no-explicit-any': ['off'], // We use this pattern extensively for block (e.g. controls_if) interfaces. '@typescript-eslint/no-empty-object-type': ['off'], - + // TSDoc doesn't support @yields, so don't require that we use it. + 'jsdoc/require-yields': ['off'], // params and returns docs are optional. 'jsdoc/require-param-description': ['off'], 'jsdoc/require-returns': ['off'], @@ -184,7 +186,7 @@ export default [ files: [ 'eslint.config.mjs', '.prettierrc.js', - 'gulpfile.js', + 'gulpfile.mjs', 'scripts/helpers.js', 'tests/mocha/.mocharc.js', 'tests/migration/validate-renamings.mjs', @@ -200,6 +202,9 @@ export default [ }, { files: ['tests/**'], + plugins: { + mocha: mochaPlugin, + }, languageOptions: { globals: { 'Blockly': true, @@ -219,6 +224,7 @@ export default [ 'jsdoc/check-tag-names': ['warn', {'definedTags': ['record']}], 'jsdoc/tag-lines': ['off'], 'jsdoc/no-defaults': ['off'], + 'mocha/no-exclusive-tests': 'error', }, }, { diff --git a/generators/dart.ts b/packages/blockly/generators/dart.ts similarity index 100% rename from generators/dart.ts rename to packages/blockly/generators/dart.ts diff --git a/generators/dart/dart_generator.ts b/packages/blockly/generators/dart/dart_generator.ts similarity index 99% rename from generators/dart/dart_generator.ts rename to packages/blockly/generators/dart/dart_generator.ts index 20feeda6f41..282e5f51904 100644 --- a/generators/dart/dart_generator.ts +++ b/packages/blockly/generators/dart/dart_generator.ts @@ -232,7 +232,7 @@ export class DartGenerator extends CodeGenerator { let comment = block.getCommentText(); if (comment) { comment = stringUtils.wrap(comment, this.COMMENT_WRAP - 3); - if ((block as AnyDuringMigration).getProcedureDef) { + if ('getProcedureDef' in block) { // Use documentation comment for function comments. commentCode += this.prefixLines(comment + '\n', '/// '); } else { diff --git a/generators/dart/lists.ts b/packages/blockly/generators/dart/lists.ts similarity index 100% rename from generators/dart/lists.ts rename to packages/blockly/generators/dart/lists.ts diff --git a/generators/dart/logic.ts b/packages/blockly/generators/dart/logic.ts similarity index 100% rename from generators/dart/logic.ts rename to packages/blockly/generators/dart/logic.ts diff --git a/generators/dart/loops.ts b/packages/blockly/generators/dart/loops.ts similarity index 100% rename from generators/dart/loops.ts rename to packages/blockly/generators/dart/loops.ts diff --git a/generators/dart/math.ts b/packages/blockly/generators/dart/math.ts similarity index 100% rename from generators/dart/math.ts rename to packages/blockly/generators/dart/math.ts diff --git a/generators/dart/procedures.ts b/packages/blockly/generators/dart/procedures.ts similarity index 100% rename from generators/dart/procedures.ts rename to packages/blockly/generators/dart/procedures.ts diff --git a/generators/dart/text.ts b/packages/blockly/generators/dart/text.ts similarity index 100% rename from generators/dart/text.ts rename to packages/blockly/generators/dart/text.ts diff --git a/generators/dart/variables.ts b/packages/blockly/generators/dart/variables.ts similarity index 100% rename from generators/dart/variables.ts rename to packages/blockly/generators/dart/variables.ts diff --git a/generators/dart/variables_dynamic.ts b/packages/blockly/generators/dart/variables_dynamic.ts similarity index 100% rename from generators/dart/variables_dynamic.ts rename to packages/blockly/generators/dart/variables_dynamic.ts diff --git a/generators/javascript.ts b/packages/blockly/generators/javascript.ts similarity index 100% rename from generators/javascript.ts rename to packages/blockly/generators/javascript.ts diff --git a/generators/javascript/javascript_generator.ts b/packages/blockly/generators/javascript/javascript_generator.ts similarity index 100% rename from generators/javascript/javascript_generator.ts rename to packages/blockly/generators/javascript/javascript_generator.ts diff --git a/generators/javascript/lists.ts b/packages/blockly/generators/javascript/lists.ts similarity index 100% rename from generators/javascript/lists.ts rename to packages/blockly/generators/javascript/lists.ts diff --git a/generators/javascript/logic.ts b/packages/blockly/generators/javascript/logic.ts similarity index 100% rename from generators/javascript/logic.ts rename to packages/blockly/generators/javascript/logic.ts diff --git a/generators/javascript/loops.ts b/packages/blockly/generators/javascript/loops.ts similarity index 100% rename from generators/javascript/loops.ts rename to packages/blockly/generators/javascript/loops.ts diff --git a/generators/javascript/math.ts b/packages/blockly/generators/javascript/math.ts similarity index 100% rename from generators/javascript/math.ts rename to packages/blockly/generators/javascript/math.ts diff --git a/generators/javascript/procedures.ts b/packages/blockly/generators/javascript/procedures.ts similarity index 100% rename from generators/javascript/procedures.ts rename to packages/blockly/generators/javascript/procedures.ts diff --git a/generators/javascript/text.ts b/packages/blockly/generators/javascript/text.ts similarity index 100% rename from generators/javascript/text.ts rename to packages/blockly/generators/javascript/text.ts diff --git a/generators/javascript/variables.ts b/packages/blockly/generators/javascript/variables.ts similarity index 100% rename from generators/javascript/variables.ts rename to packages/blockly/generators/javascript/variables.ts diff --git a/generators/javascript/variables_dynamic.ts b/packages/blockly/generators/javascript/variables_dynamic.ts similarity index 100% rename from generators/javascript/variables_dynamic.ts rename to packages/blockly/generators/javascript/variables_dynamic.ts diff --git a/generators/lua.ts b/packages/blockly/generators/lua.ts similarity index 100% rename from generators/lua.ts rename to packages/blockly/generators/lua.ts diff --git a/generators/lua/lists.ts b/packages/blockly/generators/lua/lists.ts similarity index 100% rename from generators/lua/lists.ts rename to packages/blockly/generators/lua/lists.ts diff --git a/generators/lua/logic.ts b/packages/blockly/generators/lua/logic.ts similarity index 100% rename from generators/lua/logic.ts rename to packages/blockly/generators/lua/logic.ts diff --git a/generators/lua/loops.ts b/packages/blockly/generators/lua/loops.ts similarity index 100% rename from generators/lua/loops.ts rename to packages/blockly/generators/lua/loops.ts diff --git a/generators/lua/lua_generator.ts b/packages/blockly/generators/lua/lua_generator.ts similarity index 100% rename from generators/lua/lua_generator.ts rename to packages/blockly/generators/lua/lua_generator.ts diff --git a/generators/lua/math.ts b/packages/blockly/generators/lua/math.ts similarity index 100% rename from generators/lua/math.ts rename to packages/blockly/generators/lua/math.ts diff --git a/generators/lua/procedures.ts b/packages/blockly/generators/lua/procedures.ts similarity index 100% rename from generators/lua/procedures.ts rename to packages/blockly/generators/lua/procedures.ts diff --git a/generators/lua/text.ts b/packages/blockly/generators/lua/text.ts similarity index 100% rename from generators/lua/text.ts rename to packages/blockly/generators/lua/text.ts diff --git a/generators/lua/variables.ts b/packages/blockly/generators/lua/variables.ts similarity index 100% rename from generators/lua/variables.ts rename to packages/blockly/generators/lua/variables.ts diff --git a/generators/lua/variables_dynamic.ts b/packages/blockly/generators/lua/variables_dynamic.ts similarity index 100% rename from generators/lua/variables_dynamic.ts rename to packages/blockly/generators/lua/variables_dynamic.ts diff --git a/generators/php.ts b/packages/blockly/generators/php.ts similarity index 100% rename from generators/php.ts rename to packages/blockly/generators/php.ts diff --git a/generators/php/lists.ts b/packages/blockly/generators/php/lists.ts similarity index 100% rename from generators/php/lists.ts rename to packages/blockly/generators/php/lists.ts diff --git a/generators/php/logic.ts b/packages/blockly/generators/php/logic.ts similarity index 100% rename from generators/php/logic.ts rename to packages/blockly/generators/php/logic.ts diff --git a/generators/php/loops.ts b/packages/blockly/generators/php/loops.ts similarity index 100% rename from generators/php/loops.ts rename to packages/blockly/generators/php/loops.ts diff --git a/generators/php/math.ts b/packages/blockly/generators/php/math.ts similarity index 100% rename from generators/php/math.ts rename to packages/blockly/generators/php/math.ts diff --git a/generators/php/php_generator.ts b/packages/blockly/generators/php/php_generator.ts similarity index 100% rename from generators/php/php_generator.ts rename to packages/blockly/generators/php/php_generator.ts diff --git a/generators/php/procedures.ts b/packages/blockly/generators/php/procedures.ts similarity index 99% rename from generators/php/procedures.ts rename to packages/blockly/generators/php/procedures.ts index bad6c1443aa..c881da281e2 100644 --- a/generators/php/procedures.ts +++ b/packages/blockly/generators/php/procedures.ts @@ -25,7 +25,7 @@ export function procedures_defreturn(block: Block, generator: PhpGenerator) { const workspace = block.workspace; const usedVariables = Variables.allUsedVarModels(workspace) || []; for (const variable of usedVariables) { - const varName = variable.name; + const varName = variable.getName(); // getVars returns parameter names, not ids, for procedure blocks if (!block.getVars().includes(varName)) { globals.push(generator.getVariableName(varName)); diff --git a/generators/php/text.ts b/packages/blockly/generators/php/text.ts similarity index 100% rename from generators/php/text.ts rename to packages/blockly/generators/php/text.ts diff --git a/generators/php/variables.ts b/packages/blockly/generators/php/variables.ts similarity index 100% rename from generators/php/variables.ts rename to packages/blockly/generators/php/variables.ts diff --git a/generators/php/variables_dynamic.ts b/packages/blockly/generators/php/variables_dynamic.ts similarity index 100% rename from generators/php/variables_dynamic.ts rename to packages/blockly/generators/php/variables_dynamic.ts diff --git a/generators/python.ts b/packages/blockly/generators/python.ts similarity index 100% rename from generators/python.ts rename to packages/blockly/generators/python.ts diff --git a/generators/python/lists.ts b/packages/blockly/generators/python/lists.ts similarity index 100% rename from generators/python/lists.ts rename to packages/blockly/generators/python/lists.ts diff --git a/generators/python/logic.ts b/packages/blockly/generators/python/logic.ts similarity index 100% rename from generators/python/logic.ts rename to packages/blockly/generators/python/logic.ts diff --git a/generators/python/loops.ts b/packages/blockly/generators/python/loops.ts similarity index 100% rename from generators/python/loops.ts rename to packages/blockly/generators/python/loops.ts diff --git a/generators/python/math.ts b/packages/blockly/generators/python/math.ts similarity index 100% rename from generators/python/math.ts rename to packages/blockly/generators/python/math.ts diff --git a/generators/python/procedures.ts b/packages/blockly/generators/python/procedures.ts similarity index 99% rename from generators/python/procedures.ts rename to packages/blockly/generators/python/procedures.ts index 32eae97b9e0..9c00a7d50f1 100644 --- a/generators/python/procedures.ts +++ b/packages/blockly/generators/python/procedures.ts @@ -25,7 +25,7 @@ export function procedures_defreturn(block: Block, generator: PythonGenerator) { const workspace = block.workspace; const usedVariables = Variables.allUsedVarModels(workspace) || []; for (const variable of usedVariables) { - const varName = variable.name; + const varName = variable.getName(); // getVars returns parameter names, not ids, for procedure blocks if (!block.getVars().includes(varName)) { globals.push(generator.getVariableName(varName)); diff --git a/generators/python/python_generator.ts b/packages/blockly/generators/python/python_generator.ts similarity index 100% rename from generators/python/python_generator.ts rename to packages/blockly/generators/python/python_generator.ts diff --git a/generators/python/text.ts b/packages/blockly/generators/python/text.ts similarity index 100% rename from generators/python/text.ts rename to packages/blockly/generators/python/text.ts diff --git a/generators/python/variables.ts b/packages/blockly/generators/python/variables.ts similarity index 100% rename from generators/python/variables.ts rename to packages/blockly/generators/python/variables.ts diff --git a/generators/python/variables_dynamic.ts b/packages/blockly/generators/python/variables_dynamic.ts similarity index 100% rename from generators/python/variables_dynamic.ts rename to packages/blockly/generators/python/variables_dynamic.ts diff --git a/packages/blockly/gulpfile.mjs b/packages/blockly/gulpfile.mjs new file mode 100644 index 00000000000..ad61bcb516d --- /dev/null +++ b/packages/blockly/gulpfile.mjs @@ -0,0 +1,100 @@ +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview Gulp script to build Blockly for Node & NPM. + * Run this script by calling "npm install" in this directory. + */ +/* eslint-env node */ + +// Needed to prevent prettier from munging exports order, due to +// https://github.com/simonhaenisch/prettier-plugin-organize-imports/issues/146 +// - but has the unfortunate side effect of suppressing ordering of +// imports too: +// +// organize-imports-ignore + +import {parallel} from 'gulp'; +import { + deployDemos, + deployDemosBeta, + prepareDemos, +} from './scripts/gulpfiles/appengine_tasks.mjs'; +import { + build, + buildAdvancedCompilationTest, + cleanBuildDir, + langfiles, + messages, + minify, + tsc, +} from './scripts/gulpfiles/build_tasks.mjs'; +import {docs} from './scripts/gulpfiles/docs_tasks.mjs'; +import { + createRC, + syncDevelop, + syncMaster, + updateGithubPages, +} from './scripts/gulpfiles/git_tasks.mjs'; +import {cleanReleaseDir, pack} from './scripts/gulpfiles/package_tasks.mjs'; +import { + publish, + publishBeta, + recompile, +} from './scripts/gulpfiles/release_tasks.mjs'; +import { + generators, + interactiveMocha, + test, +} from './scripts/gulpfiles/test_tasks.mjs'; + +const clean = parallel(cleanBuildDir, cleanReleaseDir); + +// Default target if gulp invoked without specifying. +export default build; + +// Main sequence targets. They already invoke prerequisites. Listed +// in typical order of invocation, and strictly listing prerequisites +// before dependants. +// +// prettier-ignore +export { + langfiles, + tsc, + minify, + build, + pack, // Formerly package. + publishBeta, + publish, + prepareDemos, + deployDemosBeta, + deployDemos, + updateGithubPages as gitUpdateGithubPages, +} + +// Manually-invokable targets that also invoke prerequisites where +// required. +// +// prettier-ignore +export { + messages, // Generate msg/json/en.json et al. + clean, + test, + generators as testGenerators, + interactiveMocha, + buildAdvancedCompilationTest, + createRC as gitCreateRC, + docs, +} + +// Legacy targets, to be deleted. +// +// prettier-ignore +export { + recompile, + syncDevelop as gitSyncDevelop, + syncMaster as gitSyncMaster, +} diff --git a/jsconfig.json b/packages/blockly/jsconfig.json similarity index 100% rename from jsconfig.json rename to packages/blockly/jsconfig.json diff --git a/media/1x1.gif b/packages/blockly/media/1x1.gif similarity index 100% rename from media/1x1.gif rename to packages/blockly/media/1x1.gif diff --git a/media/click.mp3 b/packages/blockly/media/click.mp3 similarity index 100% rename from media/click.mp3 rename to packages/blockly/media/click.mp3 diff --git a/media/delete-icon.svg b/packages/blockly/media/delete-icon.svg similarity index 100% rename from media/delete-icon.svg rename to packages/blockly/media/delete-icon.svg diff --git a/media/delete.mp3 b/packages/blockly/media/delete.mp3 similarity index 100% rename from media/delete.mp3 rename to packages/blockly/media/delete.mp3 diff --git a/media/disconnect.mp3 b/packages/blockly/media/disconnect.mp3 similarity index 100% rename from media/disconnect.mp3 rename to packages/blockly/media/disconnect.mp3 diff --git a/media/dropdown-arrow.svg b/packages/blockly/media/dropdown-arrow.svg similarity index 100% rename from media/dropdown-arrow.svg rename to packages/blockly/media/dropdown-arrow.svg diff --git a/media/foldout-icon.svg b/packages/blockly/media/foldout-icon.svg similarity index 100% rename from media/foldout-icon.svg rename to packages/blockly/media/foldout-icon.svg diff --git a/media/handclosed.cur b/packages/blockly/media/handclosed.cur similarity index 100% rename from media/handclosed.cur rename to packages/blockly/media/handclosed.cur diff --git a/media/handdelete.cur b/packages/blockly/media/handdelete.cur similarity index 100% rename from media/handdelete.cur rename to packages/blockly/media/handdelete.cur diff --git a/media/handopen.cur b/packages/blockly/media/handopen.cur similarity index 100% rename from media/handopen.cur rename to packages/blockly/media/handopen.cur diff --git a/media/pilcrow.png b/packages/blockly/media/pilcrow.png similarity index 100% rename from media/pilcrow.png rename to packages/blockly/media/pilcrow.png diff --git a/media/quote0.png b/packages/blockly/media/quote0.png similarity index 100% rename from media/quote0.png rename to packages/blockly/media/quote0.png diff --git a/media/quote1.png b/packages/blockly/media/quote1.png similarity index 100% rename from media/quote1.png rename to packages/blockly/media/quote1.png diff --git a/media/resize-handle.svg b/packages/blockly/media/resize-handle.svg similarity index 100% rename from media/resize-handle.svg rename to packages/blockly/media/resize-handle.svg diff --git a/media/sprites.png b/packages/blockly/media/sprites.png similarity index 100% rename from media/sprites.png rename to packages/blockly/media/sprites.png diff --git a/media/sprites.svg b/packages/blockly/media/sprites.svg similarity index 100% rename from media/sprites.svg rename to packages/blockly/media/sprites.svg diff --git a/msg/json/README.md b/packages/blockly/msg/json/README.md similarity index 100% rename from msg/json/README.md rename to packages/blockly/msg/json/README.md diff --git a/msg/json/ab.json b/packages/blockly/msg/json/ab.json similarity index 100% rename from msg/json/ab.json rename to packages/blockly/msg/json/ab.json diff --git a/msg/json/ace.json b/packages/blockly/msg/json/ace.json similarity index 100% rename from msg/json/ace.json rename to packages/blockly/msg/json/ace.json diff --git a/msg/json/af.json b/packages/blockly/msg/json/af.json similarity index 100% rename from msg/json/af.json rename to packages/blockly/msg/json/af.json diff --git a/msg/json/am.json b/packages/blockly/msg/json/am.json similarity index 100% rename from msg/json/am.json rename to packages/blockly/msg/json/am.json diff --git a/msg/json/ar.json b/packages/blockly/msg/json/ar.json similarity index 99% rename from msg/json/ar.json rename to packages/blockly/msg/json/ar.json index 2d382ddcf99..bad3d37ca13 100644 --- a/msg/json/ar.json +++ b/packages/blockly/msg/json/ar.json @@ -1,7 +1,6 @@ { "@metadata": { "authors": [ - "Amire80", "Diyariq", "DonAdnan", "Dr-Taher", @@ -91,7 +90,7 @@ "CONTROLS_IF_TOOLTIP_4": "إذا كانت القيمة الأولى تساوي \"صحيح\", إذن قم بتنفيذ القطعة الأولى من الأوامر. والا , إذا كانت القيمة الثانية تساوي \"صحيح\", قم بتنفيذ القطعة الثانية من الأوامر. إذا لم تكن هناك أي قيمة تساوي صحيح, قم بتنفيذ آخر قطعة من الأوامر.", "CONTROLS_IF_MSG_IF": "إذا", "CONTROLS_IF_MSG_ELSEIF": "وإﻻ إذا", - "CONTROLS_IF_MSG_ELSE": "والا", + "CONTROLS_IF_MSG_ELSE": "و إلا", "CONTROLS_IF_IF_TOOLTIP": "أضف, إزل, أو أعد ترتيب المقاطع لإعادة تكوين القطعة الشرطية \"إذا\".", "CONTROLS_IF_ELSEIF_TOOLTIP": "إضف شرطا إلى القطعة الشرطية \"إذا\".", "CONTROLS_IF_ELSE_TOOLTIP": "أضف شرط \"نهاية، إجمع\" إلى القطعة الشرطية \"إذا\".", @@ -152,7 +151,7 @@ "MATH_CONSTANT_HELPURL": "https://ar.wikipedia.org/wiki/ثابت رياضي", "MATH_CONSTANT_TOOLTIP": "ير جع واحد من الثوابت الشائعة : π (3.141…), e (2.718…), φ (1.618…), sqrt(2) (1.414…), sqrt(½) (0.707…), or ∞ (infinity).", "MATH_IS_EVEN": "هو زوجي", - "MATH_IS_ODD": "هو فرذي", + "MATH_IS_ODD": "هو فردي", "MATH_IS_PRIME": "هو أولي", "MATH_IS_WHOLE": "هو صحيح", "MATH_IS_POSITIVE": "هو موجب", @@ -328,6 +327,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "القيام بشيء ما", "PROCEDURES_BEFORE_PARAMS": "مع:", "PROCEDURES_CALL_BEFORE_PARAMS": "مع:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "لا يمكن تشغيل الوظيفة المحددة من قبل المستخدم '%1' لأن كتلة التعريف معطلة.", "PROCEDURES_DEFNORETURN_TOOLTIP": "انشئ دالة بدون مخرجات .", "PROCEDURES_DEFNORETURN_COMMENT": "صف هذه الوظيفة...", "PROCEDURES_DEFRETURN_HELPURL": "https://tr.wikipedia.org/wiki/دالة_(برمجة)", diff --git a/msg/json/ast.json b/packages/blockly/msg/json/ast.json similarity index 100% rename from msg/json/ast.json rename to packages/blockly/msg/json/ast.json diff --git a/msg/json/az.json b/packages/blockly/msg/json/az.json similarity index 98% rename from msg/json/az.json rename to packages/blockly/msg/json/az.json index edf7ff7620b..b179b6a10bd 100644 --- a/msg/json/az.json +++ b/packages/blockly/msg/json/az.json @@ -5,12 +5,14 @@ "Adil", "Cekli829", "Masalli qasimli", - "Şeyx Şamil" + "Şeyx Şamil", + "Əkrəm", + "Əkrəm Cəfər" ] }, "VARIABLES_DEFAULT_NAME": "element", "UNNAMED_KEY": "adsız", - "TODAY": "Bugün", + "TODAY": "Bu gün", "DUPLICATE_BLOCK": "Dublikat", "ADD_COMMENT": "Şərh əlavə et", "REMOVE_COMMENT": "Şərhi sil", @@ -220,8 +222,8 @@ "TEXT_TRIM_OPERATOR_RIGHT": "boşluqları yalnız sağ tərəfdən pozun", "TEXT_PRINT_TITLE": "%1 - i çap elə", "TEXT_PRINT_TOOLTIP": "Təyin olunmuş mətn, ədəd və ya hər hansı bir başqa elementi çap elə.", - "TEXT_PROMPT_TYPE_TEXT": "İstifadəçiyə mətn daxil etməsi üçün sorğunu/tələbi ismarıc ilə göndərin", - "TEXT_PROMPT_TYPE_NUMBER": "İstifadəçiyə ədəd daxil etməsi üçün sorğunu/tələbi ismarıc kimi göndərin", + "TEXT_PROMPT_TYPE_TEXT": "mesaj ehtiva edən mətn sorğusu", + "TEXT_PROMPT_TYPE_NUMBER": "mesaj ehtiva edən ədəd sorğusu", "TEXT_PROMPT_TOOLTIP_NUMBER": "İstifadəçiyə ədəd daxil etməsi üçün sorğu/tələb göndərin.", "TEXT_PROMPT_TOOLTIP_TEXT": "İstifadəçiyə mətn daxil etməsi üçün sorğu/tələb göndərin.", "TEXT_COUNT_MESSAGE0": "%2 içindən %1 sayını hesabla", diff --git a/msg/json/ba.json b/packages/blockly/msg/json/ba.json similarity index 100% rename from msg/json/ba.json rename to packages/blockly/msg/json/ba.json diff --git a/msg/json/bcc.json b/packages/blockly/msg/json/bcc.json similarity index 100% rename from msg/json/bcc.json rename to packages/blockly/msg/json/bcc.json diff --git a/msg/json/be-tarask.json b/packages/blockly/msg/json/be-tarask.json similarity index 98% rename from msg/json/be-tarask.json rename to packages/blockly/msg/json/be-tarask.json index 6d5e0ff1913..b3959de94a4 100644 --- a/msg/json/be-tarask.json +++ b/packages/blockly/msg/json/be-tarask.json @@ -10,15 +10,15 @@ "VARIABLES_DEFAULT_NAME": "аб’ект", "UNNAMED_KEY": "безназоўны", "TODAY": "Сёньня", - "DUPLICATE_BLOCK": "Капіяваць", + "DUPLICATE_BLOCK": "Дубляваць", "ADD_COMMENT": "Дадаць камэнтар", "REMOVE_COMMENT": "Выдаліць камэнтар", "DUPLICATE_COMMENT": "Прадубляваць камэнтар", "EXTERNAL_INPUTS": "Зьнешнія ўваходы", - "INLINE_INPUTS": "Унутраныя ўваходы", + "INLINE_INPUTS": "Убудаваныя ўваходы", "DELETE_BLOCK": "Выдаліць блёк", "DELETE_X_BLOCKS": "Выдаліць %1 блёкі", - "DELETE_ALL_BLOCKS": "Выдаліць усе блёкі %1?", + "DELETE_ALL_BLOCKS": "Выдаліць усе %1 блёкі?", "CLEAN_UP": "Ачысьціць блёкі", "COLLAPSE_BLOCK": "Згарнуць блёк", "COLLAPSE_ALL": "Згарнуць блёкі", @@ -31,11 +31,11 @@ "REDO": "Паўтарыць", "CHANGE_VALUE_TITLE": "Зьмяніць значэньне:", "RENAME_VARIABLE": "Перайменаваць зьменную…", - "RENAME_VARIABLE_TITLE": "Перайменаваць усе назвы зьменных '%1' на:", + "RENAME_VARIABLE_TITLE": "Перайменаваць усе зьменныя '%1' на:", "NEW_VARIABLE": "Стварыць зьменную…", "NEW_STRING_VARIABLE": "Стварыць радковую зьменную…", "NEW_NUMBER_VARIABLE": "Стварыць лікавую зьменную…", - "NEW_COLOUR_VARIABLE": "Стварыць зьменную колеру…", + "NEW_COLOUR_VARIABLE": "Стварыць колерную зьменную…", "NEW_VARIABLE_TYPE_TITLE": "Новы тып зьменнай:", "NEW_VARIABLE_TITLE": "Імя новай зьменнай:", "VARIABLE_ALREADY_EXISTS": "Зьменная з назвай «%1» ужо існуе.", @@ -310,6 +310,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "зрабіць што-небудзь", "PROCEDURES_BEFORE_PARAMS": "з:", "PROCEDURES_CALL_BEFORE_PARAMS": "з:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Немагчыма запусьціць карыстальніцкую функцыю '%1', бо адключаны блёк вызначэньня.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Стварае функцыю бяз выніку.", "PROCEDURES_DEFNORETURN_COMMENT": "Апішыце гэтую функцыю…", "PROCEDURES_DEFRETURN_RETURN": "вярнуць", diff --git a/msg/json/be.json b/packages/blockly/msg/json/be.json similarity index 99% rename from msg/json/be.json rename to packages/blockly/msg/json/be.json index 3b4954ed16e..a7e26cb7902 100644 --- a/msg/json/be.json +++ b/packages/blockly/msg/json/be.json @@ -1,6 +1,7 @@ { "@metadata": { "authors": [ + "Andoti", "No Sleep till Krupki", "Plaga med", "SimondR", @@ -8,6 +9,7 @@ ] }, "VARIABLES_DEFAULT_NAME": "элемент", + "UNNAMED_KEY": "неназваны", "TODAY": "Сёння", "DUPLICATE_BLOCK": "Дубляваць", "ADD_COMMENT": "Дадаць каментарый", diff --git a/msg/json/bg.json b/packages/blockly/msg/json/bg.json similarity index 98% rename from msg/json/bg.json rename to packages/blockly/msg/json/bg.json index 66cfde41a99..29f4d78bec5 100644 --- a/msg/json/bg.json +++ b/packages/blockly/msg/json/bg.json @@ -4,6 +4,7 @@ "Alpinistbg", "Gkostov", "InsomniHat", + "MTongov", "Miroslav35232", "NikiTricky", "ShockD", @@ -153,7 +154,7 @@ "MATH_IS_TOOLTIP": "Проверете дали дадено число е четно, нечетно, просто, цяло, положително, отрицателно или дали се дели на друго число. Връща вярно или невярно.", "MATH_CHANGE_HELPURL": "https://bg.wikipedia.org/wiki/Събиране", "MATH_CHANGE_TITLE": "промени %1 на %2", - "MATH_CHANGE_TOOLTIP": "Добави число към променлива „%1“.", + "MATH_CHANGE_TOOLTIP": "Добави число към променлива '%1'.", "MATH_ROUND_TOOLTIP": "Закръгли число нагоре или надолу.", "MATH_ROUND_OPERATOR_ROUND": "закръгли", "MATH_ROUND_OPERATOR_ROUNDUP": "закръгли нагоре", @@ -292,7 +293,7 @@ "LISTS_GET_SUBLIST_START_FROM_START": "вземи подсписък от №", "LISTS_GET_SUBLIST_START_FROM_END": "вземи подсписък от № от края", "LISTS_GET_SUBLIST_START_FIRST": "вземи подсписък от първия", - "LISTS_GET_SUBLIST_END_FROM_START": "до №", + "LISTS_GET_SUBLIST_END_FROM_START": "до #", "LISTS_GET_SUBLIST_END_FROM_END": "до № открая", "LISTS_GET_SUBLIST_END_LAST": "до края", "LISTS_GET_SUBLIST_TOOLTIP": "Копира част от списък.", @@ -319,6 +320,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "направиш", "PROCEDURES_BEFORE_PARAMS": "с:", "PROCEDURES_CALL_BEFORE_PARAMS": "с:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Не може да се изпълни дефинираната от потребителя функция '%1', защото дефиниращият блок е деактивиран.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Създава функция, която не връща резултат.", "PROCEDURES_DEFNORETURN_COMMENT": "Опишете тази функция...", "PROCEDURES_DEFRETURN_RETURN": "върни", diff --git a/msg/json/bn.json b/packages/blockly/msg/json/bn.json similarity index 100% rename from msg/json/bn.json rename to packages/blockly/msg/json/bn.json diff --git a/msg/json/br.json b/packages/blockly/msg/json/br.json similarity index 100% rename from msg/json/br.json rename to packages/blockly/msg/json/br.json diff --git a/msg/json/bs.json b/packages/blockly/msg/json/bs.json similarity index 100% rename from msg/json/bs.json rename to packages/blockly/msg/json/bs.json diff --git a/msg/json/ca.json b/packages/blockly/msg/json/ca.json similarity index 100% rename from msg/json/ca.json rename to packages/blockly/msg/json/ca.json diff --git a/msg/json/cdo.json b/packages/blockly/msg/json/cdo.json similarity index 100% rename from msg/json/cdo.json rename to packages/blockly/msg/json/cdo.json diff --git a/msg/json/ce.json b/packages/blockly/msg/json/ce.json similarity index 92% rename from msg/json/ce.json rename to packages/blockly/msg/json/ce.json index a6937aa748f..af9efea029a 100644 --- a/msg/json/ce.json +++ b/packages/blockly/msg/json/ce.json @@ -1,6 +1,7 @@ { "@metadata": { "authors": [ + "Исмаил Садуев", "Саид Мисарбиев", "Умар" ] @@ -173,20 +174,20 @@ "MATH_ATAN2_TOOLTIP": "Йухайерхайо меттиг (X, Y) арктангенс градусашкахь -180 тӀера 180 кхаччалц.", "TEXT_TEXT_TOOLTIP": "Элп, дош, могӀам йозанехь.", "TEXT_JOIN_TITLE_CREATEWITH": "кхолла могӀам", - "TEXT_JOIN_TOOLTIP": "Вовшах тосу муьлхха дакъалган терахь, кхуллу йозанан кийсиг", + "TEXT_JOIN_TOOLTIP": "Вовшахтосу муьлха дакъалган терахь, кхуллу текстан кийсиг", "TEXT_CREATE_JOIN_TITLE_JOIN": "вовшахтаса", "TEXT_CREATE_JOIN_TOOLTIP": "ТӀетоха, дӀайаккха, дехьайаккха блок йухайан дакъалгаш.", - "TEXT_CREATE_JOIN_ITEM_TOOLTIP": "ТӀетоха йозанан дакъалг", - "TEXT_APPEND_TITLE": "%1 тӀетоха йоза %2", - "TEXT_APPEND_TOOLTIP": "ТӀетоха йоза хийцалучунна \"%1\"", + "TEXT_CREATE_JOIN_ITEM_TOOLTIP": "ТӀетоха текстан дакъалг", + "TEXT_APPEND_TITLE": "%1 тӀетоха текст %2", + "TEXT_APPEND_TOOLTIP": "ТӀетоха текст хийцалучунна \"%1\"", "TEXT_LENGTH_TITLE": "дохалла %1", "TEXT_LENGTH_TOOLTIP": "Йухадерзадо сийлаллин терахь деллачу йозанехь", "TEXT_ISEMPTY_TITLE": "%1 деса ду", - "TEXT_ISEMPTY_TOOLTIP": "Нагахь йоза деса делахь, йухадерзадо бакъ долу маьӀна.", - "TEXT_INDEXOF_TOOLTIP": "Йухайерзайо лоьмар хьалхара/тӀаьххьара хьалхара йоза шолгӀачун чуваларан меттиган. Йухайерзайо %1, нагахь йоза ца карийнехь.", + "TEXT_ISEMPTY_TOOLTIP": "Нагахь текст йаьсса йелахь, йухадерзадо бакъ долу маьӀна.", + "TEXT_INDEXOF_TOOLTIP": "Йухайерзайо позицин лоьмар хьалхара/тӀаьххьара хьалхара текст шолгӀачун йукъа йахар. Йухайерзайо %1, нагахь текст ца карийнехь.", "TEXT_INDEXOF_TITLE": "Йозанехь %1, %2, %3", "TEXT_INDEXOF_OPERATOR_FIRST": "Караде хьалхара йозанера чувалар", - "TEXT_INDEXOF_OPERATOR_LAST": "караде тӀаьххьара чувалар йозанан", + "TEXT_INDEXOF_OPERATOR_LAST": "караде тӀаьххьара текстан йукъайар", "TEXT_CHARAT_TITLE": "Йозанехь %1, %2", "TEXT_CHARAT_FROM_START": "№ йолу элп схьаэца", "TEXT_CHARAT_FROM_END": "№ йолу элп схьаэца тӀехььара", @@ -194,7 +195,7 @@ "TEXT_CHARAT_LAST": "тӀаьххьара элп схьаэца", "TEXT_CHARAT_RANDOM": "Схаэца ца хоржуш элп", "TEXT_CHARAT_TOOLTIP": "Йухадерзадо элп гайтинчу меттиге", - "TEXT_GET_SUBSTRING_TOOLTIP": "Йозанан гайтина дакъа йухадерзадо", + "TEXT_GET_SUBSTRING_TOOLTIP": "Текстан гайтина дакъа йухадерзадо", "TEXT_GET_SUBSTRING_INPUT_IN_TEXT": "Йозанехь", "TEXT_GET_SUBSTRING_START_FROM_START": "Схьаэца бухара могӀа № йолу элпера", "TEXT_GET_SUBSTRING_START_FROM_END": "Схьаэца бухара могӀа № йолу элпера тӀехьара", @@ -202,22 +203,22 @@ "TEXT_GET_SUBSTRING_END_FROM_START": "№ йолу элп схьаэца", "TEXT_GET_SUBSTRING_END_FROM_END": "№ йолу элп схьаэца тӀехьара", "TEXT_GET_SUBSTRING_END_LAST": "тӀаьххьара элп схьаэца", - "TEXT_CHANGECASE_TOOLTIP": "Йухайерзайо йозанан копи ХЬАЛХАРЧУ йа могӀанан элпашца", + "TEXT_CHANGECASE_TOOLTIP": "Йухайерзайо текстан копи ХЬАЛХАРЧУ йа могӀанан элпашца", "TEXT_CHANGECASE_OPERATOR_UPPERCASE": "ХЬАЛХАРЧУ ЭЛПАШКА", "TEXT_CHANGECASE_OPERATOR_LOWERCASE": "могӀанан элпаш", "TEXT_CHANGECASE_OPERATOR_TITLECASE": "Хьалхарчу Коьрта Элпашка", - "TEXT_TRIM_TOOLTIP": "Йухайерзайо йозанан копи генайаьккхина йукъ цхьаьна йа шинне а йуьххьера", + "TEXT_TRIM_TOOLTIP": "Йухайерзайо текстан копи генайаьккхина йукъ цхьаьна йа шинне а йуьххьера", "TEXT_TRIM_OPERATOR_BOTH": "Йаккъаш дӀахедайар", "TEXT_TRIM_OPERATOR_LEFT": "аьрру агӀор йукъ дӀахадайар", "TEXT_TRIM_OPERATOR_RIGHT": "Аьтту агӀор йаккъаш дӀахедайе", "TEXT_PRINT_TITLE": "Зорбатоха %1", - "TEXT_PRINT_TOOLTIP": "Зорбатуху йозанан, терахьан йа кхечу объектан", - "TEXT_PROMPT_TYPE_TEXT": "йоза деха дӀааларца", + "TEXT_PRINT_TOOLTIP": "Зорбатуху текстан, терахьан йа кхечу объектан", + "TEXT_PROMPT_TYPE_TEXT": "текст деха дӀааларца", "TEXT_PROMPT_TYPE_NUMBER": "терахь деха дӀааларца", "TEXT_PROMPT_TOOLTIP_NUMBER": "Лелочунга терахь деха.", - "TEXT_PROMPT_TOOLTIP_TEXT": "Йоза деха лелочуьнга.", + "TEXT_PROMPT_TOOLTIP_TEXT": "Текст йеха декъашхочунга.", "TEXT_COUNT_MESSAGE0": "Дагарде барам %1 %2", - "TEXT_COUNT_TOOLTIP": "Дагарде, мосазза йолу йозанан кийсиг кхечу йозанехь.", + "TEXT_COUNT_TOOLTIP": "Дагарде, мосазза йолу текстан кийсиг кхечу текстехь гучуйолу.", "TEXT_REPLACE_MESSAGE0": "хийца %1 %2 %3", "TEXT_REPLACE_TOOLTIP": "Хийца ша долу дерриг йозананна чуваларш, кхечу йозанца", "TEXT_REVERSE_MESSAGE0": "хийца низам йуханехьа %1", @@ -228,7 +229,7 @@ "LISTS_CREATE_WITH_INPUT_WITH": "кхолла могӀам", "LISTS_CREATE_WITH_CONTAINER_TITLE_ADD": "Исписка", "LISTS_CREATE_WITH_CONTAINER_TOOLTIP": "ТӀетоха, дӀайаккха, дехьайаккха блок йухайан дакъалгаш.", - "LISTS_CREATE_WITH_ITEM_TOOLTIP": "ТӀетоха йозанан дакъалг", + "LISTS_CREATE_WITH_ITEM_TOOLTIP": "ТӀетоха текстан дакъалг", "LISTS_REPEAT_TOOLTIP": "Лаьтташ йолу дакъалган копин терахьех исписка кхуллу.", "LISTS_REPEAT_TITLE": "кхолла дакъалг %1 исписка, йухайзйеш йолун %2", "LISTS_LENGTH_TITLE": "дохалла %1", @@ -237,8 +238,8 @@ "LISTS_ISEMPTY_TOOLTIP": "Нагахь исписка йеса йелахь, йухадерзадо бакъдолчун маьӀна", "LISTS_INLIST": "испискехь", "LISTS_INDEX_OF_FIRST": "Караде хьалхара йозанера чувалар", - "LISTS_INDEX_OF_LAST": "караде тӀаьххьара чувалар йозанан", - "LISTS_INDEX_OF_TOOLTIP": "Йухайерзайо лоьмар хьалхара/тӀаьххьара хьалхара йоза шолгӀачун чуваларан меттиган. Йухайерзайо %1, нагахь йоза ца карийнехь.", + "LISTS_INDEX_OF_LAST": "караде тӀаьххьара текстан йуъадар", + "LISTS_INDEX_OF_TOOLTIP": "Йухайерзайо позицин лоьмар хьалхара/тӀаьххьара могӀам чу элемент йахар. Йухайерзайо %1, нагахь элемент ца карийнехь.", "LISTS_GET_INDEX_GET": "Схьаэца", "LISTS_GET_INDEX_GET_REMOVE": "Схьаэца, дӀадаккха", "LISTS_GET_INDEX_REMOVE": "дӀайаккха", @@ -246,7 +247,7 @@ "LISTS_GET_INDEX_FROM_END": "№ тӀехьара", "LISTS_GET_INDEX_FIRST": "хьалхара", "LISTS_GET_INDEX_LAST": "тӀаьххьара", - "LISTS_GET_INDEX_RANDOM": "Нисделларг", + "LISTS_GET_INDEX_RANDOM": "ма-луъу", "LISTS_INDEX_FROM_START_TOOLTIP": "%1 - хьалхара дакъалг", "LISTS_INDEX_FROM_END_TOOLTIP": "%1 - тӀаьххьара дакъалг", "LISTS_GET_INDEX_TOOLTIP_GET_FROM": "Йухайерзайо дакъалг испискан гайтинчу меттиге.", @@ -287,10 +288,10 @@ "LISTS_SORT_TYPE_TEXT": "абатца", "LISTS_SORT_TYPE_IGNORECASE": "Абатца, регистран чот а ца лелош", "LISTS_SPLIT_LIST_FROM_TEXT": "йозанах исписка йар", - "LISTS_SPLIT_TEXT_FROM_LIST": "йоза вовшахтоха испискех", + "LISTS_SPLIT_TEXT_FROM_LIST": "текст вовшахтоха могӀанах", "LISTS_SPLIT_WITH_DELIMITER": "доькъучунца", - "LISTS_SPLIT_TOOLTIP_SPLIT": "Доькъу йоза йозанан испискан доькъучуьнца", - "LISTS_SPLIT_TOOLTIP_JOIN": "Вовшахтуху йозанийн исписка цхьаьна йозане доькъучуьнца.", + "LISTS_SPLIT_TOOLTIP_SPLIT": "Йоькъу текст могӀаман текст тӀе доькъучуьнца", + "LISTS_SPLIT_TOOLTIP_JOIN": "Вовшахтуху текстийн могӀам цхьаьна тексте доькъучуьнца.", "LISTS_REVERSE_MESSAGE0": "хийца низам йуханехьа %1", "LISTS_REVERSE_TOOLTIP": "Испискан низам йуханехьа хийца", "VARIABLES_GET_TOOLTIP": "Оцу хийцалучун маьӀна хуьйцу", @@ -302,6 +303,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "кхочушдан цхьа хӀума", "PROCEDURES_BEFORE_PARAMS": "дуьйна:", "PROCEDURES_CALL_BEFORE_PARAMS": "дуьйна:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Декъашхочун '%1' функци дӀайахьа йиш йац, хӀунда аьлча билгалйаьккхина блок дӀайаьккхина йу.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Кхуллу маьӀна йуха ца дерзош йолу процедура", "PROCEDURES_DEFNORETURN_COMMENT": "Гайта и функци", "PROCEDURES_DEFRETURN_RETURN": "йухадерзо", diff --git a/msg/json/constants.json b/packages/blockly/msg/json/constants.json similarity index 100% rename from msg/json/constants.json rename to packages/blockly/msg/json/constants.json diff --git a/msg/json/cs.json b/packages/blockly/msg/json/cs.json similarity index 99% rename from msg/json/cs.json rename to packages/blockly/msg/json/cs.json index 1855d9752c6..ca17fa91eb0 100644 --- a/msg/json/cs.json +++ b/packages/blockly/msg/json/cs.json @@ -1,7 +1,6 @@ { "@metadata": { "authors": [ - "Amire80", "Chmee2", "Clon", "Dita", @@ -12,6 +11,7 @@ "Ilimanaq29", "Koo6", "Matěj Grabovský", + "Mormegil", "Patriccck", "Patrik L.", "Robins7", @@ -53,6 +53,7 @@ "NEW_VARIABLE_TITLE": "Nový název proměnné:", "VARIABLE_ALREADY_EXISTS": "Proměnná jménem '%1' již existuje", "VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE": "Proměnná pojmenovaná jako '%1' již existuje pro jiný typ: '%2'.", + "VARIABLE_ALREADY_EXISTS_FOR_A_PARAMETER": "Proměnná pojmenovaná „%1“ již jako parametr v proceduře „%2“ existuje.", "DELETE_VARIABLE_CONFIRMATION": "Odstranit %1 použití proměnné '%2'", "CANNOT_DELETE_VARIABLE_PROCEDURE": "Proměnnou '%1' není možné odstranit, protože je součástí definice funkce '%2'", "DELETE_VARIABLE": "Odstranit proměnnou '%1'", diff --git a/msg/json/da.json b/packages/blockly/msg/json/da.json similarity index 100% rename from msg/json/da.json rename to packages/blockly/msg/json/da.json diff --git a/msg/json/de.json b/packages/blockly/msg/json/de.json similarity index 98% rename from msg/json/de.json rename to packages/blockly/msg/json/de.json index 33ed7191066..914be827d3e 100644 --- a/msg/json/de.json +++ b/packages/blockly/msg/json/de.json @@ -1,7 +1,6 @@ { "@metadata": { "authors": [ - "Amire80", "Brettchenweber", "Cvanca", "Dan-yell", @@ -110,7 +109,7 @@ "LOGIC_COMPARE_TOOLTIP_GTE": "Ist wahr, falls der erste Wert größer als oder gleich groß wie der zweite Wert ist.", "LOGIC_OPERATION_TOOLTIP_AND": "Ist wahr, falls beide Werte wahr sind.", "LOGIC_OPERATION_AND": "und", - "LOGIC_OPERATION_TOOLTIP_OR": "Ist wahr, falls mindestens einer der beiden Werte wahr ist.", + "LOGIC_OPERATION_TOOLTIP_OR": "Gibt true zurück, wenn mindestens eine der Eingaben wahr ist.", "LOGIC_OPERATION_OR": "oder", "LOGIC_NEGATE_TITLE": "nicht %1", "LOGIC_NEGATE_TOOLTIP": "Ist wahr, falls der Eingabewert unwahr ist. Ist unwahr, falls der Eingabewert wahr ist.", @@ -342,6 +341,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "etwas tun", "PROCEDURES_BEFORE_PARAMS": "mit:", "PROCEDURES_CALL_BEFORE_PARAMS": "mit:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Die benutzerdefinierte Funktion '%1' kann nicht ausgeführt werden, weil der Definitionsblock deaktiviert ist.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Ein Funktionsblock ohne Rückgabewert.", "PROCEDURES_DEFNORETURN_COMMENT": "Beschreibe diese Funktion …", "PROCEDURES_DEFRETURN_HELPURL": "https://de.wikipedia.org/wiki/Prozedur_(Programmierung)", diff --git a/msg/json/diq.json b/packages/blockly/msg/json/diq.json similarity index 96% rename from msg/json/diq.json rename to packages/blockly/msg/json/diq.json index bc37d442a9c..9661e9c0c0b 100644 --- a/msg/json/diq.json +++ b/packages/blockly/msg/json/diq.json @@ -2,6 +2,7 @@ "@metadata": { "authors": [ "1917 Ekim Devrimi", + "GolyatGeri", "Gırd", "Kumkumuk", "Marmase", @@ -77,7 +78,9 @@ "CONTROLS_IF_MSG_ELSEIF": "eke nêyo", "CONTROLS_IF_MSG_ELSE": "eke çıniyo", "CONTROLS_IF_ELSEIF_TOOLTIP": "Bloq da if'i rê yu şert dekerê de.", + "CONTROLS_IF_ELSE_TOOLTIP": "Blokê if'i rê yew şerto ke her çi ihtıwa keno, cıkerê.", "LOGIC_COMPARE_TOOLTIP_EQ": "Debiyaye dı erci zey pêyêse ercê \"True\" dane.", + "LOGIC_COMPARE_TOOLTIP_NEQ": "Dı cıbiyayeyi ke zey yewbini niyê, bıçarne era raşt.", "LOGIC_OPERATION_TOOLTIP_AND": "Eger her dı cıkewtışi zi raştê, şıma ageyrê.", "LOGIC_OPERATION_AND": "û", "LOGIC_OPERATION_OR": "ya zi", @@ -129,11 +132,13 @@ "MATH_IS_NEGATIVE": "negatifo", "MATH_IS_DIVISIBLE_BY": "Leteyêno", "MATH_CHANGE_TITLE": "%2, keno %1 vurneno", + "MATH_CHANGE_TOOLTIP": "Yew amari be '%1' vurriyayoğ ra zêde ke.", "MATH_ROUND_TOOLTIP": "Yu amorer loğê cêri yana cori ke", "MATH_ROUND_OPERATOR_ROUND": "gılor ke", "MATH_ROUND_OPERATOR_ROUNDUP": "Loğê cori ke", "MATH_ROUND_OPERATOR_ROUNDDOWN": "Loğê cêri ke", "MATH_ONLIST_OPERATOR_SUM": "koma liste", + "MATH_ONLIST_TOOLTIP_SUM": "Antolociya heme amaran liste de açarne.", "MATH_ONLIST_OPERATOR_MIN": "Tewr qıcê lista", "MATH_ONLIST_TOOLTIP_MIN": "Lista de tewr qıckek amar tadê", "MATH_ONLIST_OPERATOR_MAX": "Tewr gırdê lista", @@ -151,7 +156,7 @@ "MATH_ATAN2_HELPURL": "https://diq.wikipedia.org/wiki/Atan2", "MATH_ATAN2_TITLE": "atan2, X:%1 Y:%2", "TEXT_TEXT_TOOLTIP": "Yu herfa, satır yana çekuya metini", - "TEXT_JOIN_TITLE_CREATEWITH": "ya metin vıraz", + "TEXT_JOIN_TITLE_CREATEWITH": "metın vıraze", "TEXT_CREATE_JOIN_TITLE_JOIN": "gıre de", "TEXT_CREATE_JOIN_ITEM_TOOLTIP": "Yew işaret nuşteyi ke.", "TEXT_APPEND_TITLE": "rê %1 Metin dek %2", @@ -229,7 +234,7 @@ "LISTS_SORT_TYPE_TEXT": "Alfabetik", "LISTS_SORT_TYPE_IGNORECASE": "alfabetik, nezeri mekerên", "LISTS_SPLIT_LIST_FROM_TEXT": "metini ra lista bıkerê", - "LISTS_SPLIT_TEXT_FROM_LIST": "Lista ra metin bıkerê", + "LISTS_SPLIT_TEXT_FROM_LIST": "lista ra metın vıraze", "LISTS_SPLIT_WITH_DELIMITER": "Hududoxi ya", "LISTS_REVERSE_MESSAGE0": "%1 dimlaşt kerê", "LISTS_REVERSE_TOOLTIP": "Yew kopyaya yew lista dimlaşt kerê.", @@ -253,6 +258,7 @@ "PROCEDURES_CREATE_DO": "'%1' vıraze", "WORKSPACE_COMMENT_DEFAULT_TEXT": "Çiyê vace...", "WORKSPACE_ARIA_LABEL": "Blockly Caygurenayışi", + "COLLAPSED_WARNINGS_WARNING": "Blokê xırabeyi iqazan ihtıwa kenê.", "DIALOG_OK": "TEMAM", "DIALOG_CANCEL": "Bıtexelne" } diff --git a/msg/json/dtp.json b/packages/blockly/msg/json/dtp.json similarity index 100% rename from msg/json/dtp.json rename to packages/blockly/msg/json/dtp.json diff --git a/msg/json/dty.json b/packages/blockly/msg/json/dty.json similarity index 100% rename from msg/json/dty.json rename to packages/blockly/msg/json/dty.json diff --git a/msg/json/ee.json b/packages/blockly/msg/json/ee.json similarity index 100% rename from msg/json/ee.json rename to packages/blockly/msg/json/ee.json diff --git a/msg/json/el.json b/packages/blockly/msg/json/el.json similarity index 100% rename from msg/json/el.json rename to packages/blockly/msg/json/el.json diff --git a/msg/json/en-gb.json b/packages/blockly/msg/json/en-gb.json similarity index 100% rename from msg/json/en-gb.json rename to packages/blockly/msg/json/en-gb.json diff --git a/msg/json/en.json b/packages/blockly/msg/json/en.json similarity index 82% rename from msg/json/en.json rename to packages/blockly/msg/json/en.json index 50800bc27e8..efa06f10c71 100644 --- a/msg/json/en.json +++ b/packages/blockly/msg/json/en.json @@ -1,7 +1,7 @@ { "@metadata": { "author": "Ellen Spertus ", - "lastupdated": "2024-04-16 23:19:53.668551", + "lastupdated": "2026-01-08 08:39:56.707280", "locale": "en", "messagedocumentation" : "qqq" }, @@ -18,6 +18,7 @@ "DELETE_X_BLOCKS": "Delete %1 Blocks", "DELETE_ALL_BLOCKS": "Delete all %1 blocks?", "CLEAN_UP": "Clean up Blocks", + "CLOSE": "Close", "COLLAPSE_BLOCK": "Collapse Block", "COLLAPSE_ALL": "Collapse Blocks", "EXPAND_BLOCK": "Expand Block", @@ -63,24 +64,24 @@ "CONTROLS_REPEAT_TITLE": "repeat %1 times", "CONTROLS_REPEAT_INPUT_DO": "do", "CONTROLS_REPEAT_TOOLTIP": "Do some statements several times.", - "CONTROLS_WHILEUNTIL_HELPURL": "https://github.com/google/blockly/wiki/Loops#repeat", + "CONTROLS_WHILEUNTIL_HELPURL": "https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#repeat", "CONTROLS_WHILEUNTIL_OPERATOR_WHILE": "repeat while", "CONTROLS_WHILEUNTIL_OPERATOR_UNTIL": "repeat until", "CONTROLS_WHILEUNTIL_TOOLTIP_WHILE": "While a value is true, then do some statements.", "CONTROLS_WHILEUNTIL_TOOLTIP_UNTIL": "While a value is false, then do some statements.", - "CONTROLS_FOR_HELPURL": "https://github.com/google/blockly/wiki/Loops#count-with", + "CONTROLS_FOR_HELPURL": "https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#count-with", "CONTROLS_FOR_TOOLTIP": "Have the variable '%1' take on the values from the start number to the end number, counting by the specified interval, and do the specified blocks.", "CONTROLS_FOR_TITLE": "count with %1 from %2 to %3 by %4", - "CONTROLS_FOREACH_HELPURL": "https://github.com/google/blockly/wiki/Loops#for-each", + "CONTROLS_FOREACH_HELPURL": "https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#for-each", "CONTROLS_FOREACH_TITLE": "for each item %1 in list %2", "CONTROLS_FOREACH_TOOLTIP": "For each item in a list, set the variable '%1' to the item, and then do some statements.", - "CONTROLS_FLOW_STATEMENTS_HELPURL": "https://github.com/google/blockly/wiki/Loops#loop-termination-blocks", + "CONTROLS_FLOW_STATEMENTS_HELPURL": "https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#loop-termination-blocks", "CONTROLS_FLOW_STATEMENTS_OPERATOR_BREAK": "break out of loop", "CONTROLS_FLOW_STATEMENTS_OPERATOR_CONTINUE": "continue with next iteration of loop", "CONTROLS_FLOW_STATEMENTS_TOOLTIP_BREAK": "Break out of the containing loop.", "CONTROLS_FLOW_STATEMENTS_TOOLTIP_CONTINUE": "Skip the rest of this loop, and continue with the next iteration.", "CONTROLS_FLOW_STATEMENTS_WARNING": "Warning: This block may only be used within a loop.", - "CONTROLS_IF_HELPURL": "https://github.com/google/blockly/wiki/IfElse", + "CONTROLS_IF_HELPURL": "https://github.com/RaspberryPiFoundation/blockly/wiki/IfElse", "CONTROLS_IF_TOOLTIP_1": "If a value is true, then do some statements.", "CONTROLS_IF_TOOLTIP_2": "If a value is true, then do the first block of statements. Otherwise, do the second block of statements.", "CONTROLS_IF_TOOLTIP_3": "If the first value is true, then do the first block of statements. Otherwise, if the second value is true, do the second block of statements.", @@ -98,15 +99,15 @@ "LOGIC_COMPARE_TOOLTIP_LTE": "Return true if the first input is smaller than or equal to the second input.", "LOGIC_COMPARE_TOOLTIP_GT": "Return true if the first input is greater than the second input.", "LOGIC_COMPARE_TOOLTIP_GTE": "Return true if the first input is greater than or equal to the second input.", - "LOGIC_OPERATION_HELPURL": "https://github.com/google/blockly/wiki/Logic#logical-operations", + "LOGIC_OPERATION_HELPURL": "https://github.com/RaspberryPiFoundation/blockly/wiki/Logic#logical-operations", "LOGIC_OPERATION_TOOLTIP_AND": "Return true if both inputs are true.", "LOGIC_OPERATION_AND": "and", "LOGIC_OPERATION_TOOLTIP_OR": "Return true if at least one of the inputs is true.", "LOGIC_OPERATION_OR": "or", - "LOGIC_NEGATE_HELPURL": "https://github.com/google/blockly/wiki/Logic#not", + "LOGIC_NEGATE_HELPURL": "https://github.com/RaspberryPiFoundation/blockly/wiki/Logic#not", "LOGIC_NEGATE_TITLE": "not %1", "LOGIC_NEGATE_TOOLTIP": "Returns true if the input is false. Returns false if the input is true.", - "LOGIC_BOOLEAN_HELPURL": "https://github.com/google/blockly/wiki/Logic#values", + "LOGIC_BOOLEAN_HELPURL": "https://github.com/RaspberryPiFoundation/blockly/wiki/Logic#values", "LOGIC_BOOLEAN_TRUE": "true", "LOGIC_BOOLEAN_FALSE": "false", "LOGIC_BOOLEAN_TOOLTIP": "Returns either true or false.", @@ -206,27 +207,27 @@ "MATH_ATAN2_TOOLTIP": "Return the arctangent of point (X, Y) in degrees from -180 to 180.", "TEXT_TEXT_HELPURL": "https://en.wikipedia.org/wiki/String_(computer_science)", "TEXT_TEXT_TOOLTIP": "A letter, word, or line of text.", - "TEXT_JOIN_HELPURL": "https://github.com/google/blockly/wiki/Text#text-creation", + "TEXT_JOIN_HELPURL": "https://github.com/RaspberryPiFoundation/blockly/wiki/Text#text-creation", "TEXT_JOIN_TITLE_CREATEWITH": "create text with", "TEXT_JOIN_TOOLTIP": "Create a piece of text by joining together any number of items.", "TEXT_CREATE_JOIN_TITLE_JOIN": "join", "TEXT_CREATE_JOIN_TOOLTIP": "Add, remove, or reorder sections to reconfigure this text block.", "TEXT_CREATE_JOIN_ITEM_TOOLTIP": "Add an item to the text.", - "TEXT_APPEND_HELPURL": "https://github.com/google/blockly/wiki/Text#text-modification", + "TEXT_APPEND_HELPURL": "https://github.com/RaspberryPiFoundation/blockly/wiki/Text#text-modification", "TEXT_APPEND_TITLE": "to %1 append text %2", "TEXT_APPEND_TOOLTIP": "Append some text to variable '%1'.", - "TEXT_LENGTH_HELPURL": "https://github.com/google/blockly/wiki/Text#text-modification", + "TEXT_LENGTH_HELPURL": "https://github.com/RaspberryPiFoundation/blockly/wiki/Text#text-modification", "TEXT_LENGTH_TITLE": "length of %1", "TEXT_LENGTH_TOOLTIP": "Returns the number of letters (including spaces) in the provided text.", - "TEXT_ISEMPTY_HELPURL": "https://github.com/google/blockly/wiki/Text#checking-for-empty-text", + "TEXT_ISEMPTY_HELPURL": "https://github.com/RaspberryPiFoundation/blockly/wiki/Text#checking-for-empty-text", "TEXT_ISEMPTY_TITLE": "%1 is empty", "TEXT_ISEMPTY_TOOLTIP": "Returns true if the provided text is empty.", - "TEXT_INDEXOF_HELPURL": "https://github.com/google/blockly/wiki/Text#finding-text", + "TEXT_INDEXOF_HELPURL": "https://github.com/RaspberryPiFoundation/blockly/wiki/Text#finding-text", "TEXT_INDEXOF_TOOLTIP": "Returns the index of the first/last occurrence of the first text in the second text. Returns %1 if text is not found.", "TEXT_INDEXOF_TITLE": "in text %1 %2 %3", "TEXT_INDEXOF_OPERATOR_FIRST": "find first occurrence of text", "TEXT_INDEXOF_OPERATOR_LAST": "find last occurrence of text", - "TEXT_CHARAT_HELPURL": "https://github.com/google/blockly/wiki/Text#extracting-text", + "TEXT_CHARAT_HELPURL": "https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-text", "TEXT_CHARAT_TITLE": "in text %1 %2", "TEXT_CHARAT_FROM_START": "get letter #", "TEXT_CHARAT_FROM_END": "get letter # from end", @@ -236,7 +237,7 @@ "TEXT_CHARAT_TAIL": "", "TEXT_CHARAT_TOOLTIP": "Returns the letter at the specified position.", "TEXT_GET_SUBSTRING_TOOLTIP": "Returns a specified portion of the text.", - "TEXT_GET_SUBSTRING_HELPURL": "https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text", + "TEXT_GET_SUBSTRING_HELPURL": "https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-region-of-text", "TEXT_GET_SUBSTRING_INPUT_IN_TEXT": "in text", "TEXT_GET_SUBSTRING_START_FROM_START": "get substring from letter #", "TEXT_GET_SUBSTRING_START_FROM_END": "get substring from letter # from end", @@ -245,57 +246,57 @@ "TEXT_GET_SUBSTRING_END_FROM_END": "to letter # from end", "TEXT_GET_SUBSTRING_END_LAST": "to last letter", "TEXT_GET_SUBSTRING_TAIL": "", - "TEXT_CHANGECASE_HELPURL": "https://github.com/google/blockly/wiki/Text#adjusting-text-case", + "TEXT_CHANGECASE_HELPURL": "https://github.com/RaspberryPiFoundation/blockly/wiki/Text#adjusting-text-case", "TEXT_CHANGECASE_TOOLTIP": "Return a copy of the text in a different case.", "TEXT_CHANGECASE_OPERATOR_UPPERCASE": "to UPPER CASE", "TEXT_CHANGECASE_OPERATOR_LOWERCASE": "to lower case", "TEXT_CHANGECASE_OPERATOR_TITLECASE": "to Title Case", - "TEXT_TRIM_HELPURL": "https://github.com/google/blockly/wiki/Text#trimming-removing-spaces", + "TEXT_TRIM_HELPURL": "https://github.com/RaspberryPiFoundation/blockly/wiki/Text#trimming-removing-spaces", "TEXT_TRIM_TOOLTIP": "Return a copy of the text with spaces removed from one or both ends.", "TEXT_TRIM_OPERATOR_BOTH": "trim spaces from both sides of", "TEXT_TRIM_OPERATOR_LEFT": "trim spaces from left side of", "TEXT_TRIM_OPERATOR_RIGHT": "trim spaces from right side of", - "TEXT_PRINT_HELPURL": "https://github.com/google/blockly/wiki/Text#printing-text", + "TEXT_PRINT_HELPURL": "https://github.com/RaspberryPiFoundation/blockly/wiki/Text#printing-text", "TEXT_PRINT_TITLE": "print %1", "TEXT_PRINT_TOOLTIP": "Print the specified text, number or other value.", - "TEXT_PROMPT_HELPURL": "https://github.com/google/blockly/wiki/Text#getting-input-from-the-user", + "TEXT_PROMPT_HELPURL": "https://github.com/RaspberryPiFoundation/blockly/wiki/Text#getting-input-from-the-user", "TEXT_PROMPT_TYPE_TEXT": "prompt for text with message", "TEXT_PROMPT_TYPE_NUMBER": "prompt for number with message", "TEXT_PROMPT_TOOLTIP_NUMBER": "Prompt for user for a number.", "TEXT_PROMPT_TOOLTIP_TEXT": "Prompt for user for some text.", "TEXT_COUNT_MESSAGE0": "count %1 in %2", - "TEXT_COUNT_HELPURL": "https://github.com/google/blockly/wiki/Text#counting-substrings", + "TEXT_COUNT_HELPURL": "https://github.com/RaspberryPiFoundation/blockly/wiki/Text#counting-substrings", "TEXT_COUNT_TOOLTIP": "Count how many times some text occurs within some other text.", "TEXT_REPLACE_MESSAGE0": "replace %1 with %2 in %3", - "TEXT_REPLACE_HELPURL": "https://github.com/google/blockly/wiki/Text#replacing-substrings", + "TEXT_REPLACE_HELPURL": "https://github.com/RaspberryPiFoundation/blockly/wiki/Text#replacing-substrings", "TEXT_REPLACE_TOOLTIP": "Replace all occurances of some text within some other text.", "TEXT_REVERSE_MESSAGE0": "reverse %1", - "TEXT_REVERSE_HELPURL": "https://github.com/google/blockly/wiki/Text#reversing-text", + "TEXT_REVERSE_HELPURL": "https://github.com/RaspberryPiFoundation/blockly/wiki/Text#reversing-text", "TEXT_REVERSE_TOOLTIP": "Reverses the order of the characters in the text.", - "LISTS_CREATE_EMPTY_HELPURL": "https://github.com/google/blockly/wiki/Lists#create-empty-list", + "LISTS_CREATE_EMPTY_HELPURL": "https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#create-empty-list", "LISTS_CREATE_EMPTY_TITLE": "create empty list", "LISTS_CREATE_EMPTY_TOOLTIP": "Returns a list, of length 0, containing no data records", - "LISTS_CREATE_WITH_HELPURL": "https://github.com/google/blockly/wiki/Lists#create-list-with", + "LISTS_CREATE_WITH_HELPURL": "https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#create-list-with", "LISTS_CREATE_WITH_TOOLTIP": "Create a list with any number of items.", "LISTS_CREATE_WITH_INPUT_WITH": "create list with", "LISTS_CREATE_WITH_CONTAINER_TITLE_ADD": "list", "LISTS_CREATE_WITH_CONTAINER_TOOLTIP": "Add, remove, or reorder sections to reconfigure this list block.", "LISTS_CREATE_WITH_ITEM_TOOLTIP": "Add an item to the list.", - "LISTS_REPEAT_HELPURL": "https://github.com/google/blockly/wiki/Lists#create-list-with", + "LISTS_REPEAT_HELPURL": "https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#create-list-with", "LISTS_REPEAT_TOOLTIP": "Creates a list consisting of the given value repeated the specified number of times.", "LISTS_REPEAT_TITLE": "create list with item %1 repeated %2 times", - "LISTS_LENGTH_HELPURL": "https://github.com/google/blockly/wiki/Lists#length-of", + "LISTS_LENGTH_HELPURL": "https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#length-of", "LISTS_LENGTH_TITLE": "length of %1", "LISTS_LENGTH_TOOLTIP": "Returns the length of a list.", - "LISTS_ISEMPTY_HELPURL": "https://github.com/google/blockly/wiki/Lists#is-empty", + "LISTS_ISEMPTY_HELPURL": "https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#is-empty", "LISTS_ISEMPTY_TITLE": "%1 is empty", "LISTS_ISEMPTY_TOOLTIP": "Returns true if the list is empty.", "LISTS_INLIST": "in list", - "LISTS_INDEX_OF_HELPURL": "https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list", + "LISTS_INDEX_OF_HELPURL": "https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#finding-items-in-a-list", "LISTS_INDEX_OF_FIRST": "find first occurrence of item", "LISTS_INDEX_OF_LAST": "find last occurrence of item", "LISTS_INDEX_OF_TOOLTIP": "Returns the index of the first/last occurrence of the item in the list. Returns %1 if item is not found.", - "LISTS_GET_INDEX_HELPURL": "https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list", + "LISTS_GET_INDEX_HELPURL": "https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-items-from-a-list", "LISTS_GET_INDEX_GET": "get", "LISTS_GET_INDEX_GET_REMOVE": "get and remove", "LISTS_GET_INDEX_REMOVE": "remove", @@ -319,7 +320,7 @@ "LISTS_GET_INDEX_TOOLTIP_REMOVE_FIRST": "Removes the first item in a list.", "LISTS_GET_INDEX_TOOLTIP_REMOVE_LAST": "Removes the last item in a list.", "LISTS_GET_INDEX_TOOLTIP_REMOVE_RANDOM": "Removes a random item in a list.", - "LISTS_SET_INDEX_HELPURL": "https://github.com/google/blockly/wiki/Lists#in-list--set", + "LISTS_SET_INDEX_HELPURL": "https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#in-list--set", "LISTS_SET_INDEX_SET": "set", "LISTS_SET_INDEX_INSERT": "insert at", "LISTS_SET_INDEX_INPUT_TO": "as", @@ -331,7 +332,7 @@ "LISTS_SET_INDEX_TOOLTIP_INSERT_FIRST": "Inserts the item at the start of a list.", "LISTS_SET_INDEX_TOOLTIP_INSERT_LAST": "Append the item to the end of a list.", "LISTS_SET_INDEX_TOOLTIP_INSERT_RANDOM": "Inserts the item randomly in a list.", - "LISTS_GET_SUBLIST_HELPURL": "https://github.com/google/blockly/wiki/Lists#getting-a-sublist", + "LISTS_GET_SUBLIST_HELPURL": "https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-sublist", "LISTS_GET_SUBLIST_START_FROM_START": "get sub-list from #", "LISTS_GET_SUBLIST_START_FROM_END": "get sub-list from # from end", "LISTS_GET_SUBLIST_START_FIRST": "get sub-list from first", @@ -340,7 +341,7 @@ "LISTS_GET_SUBLIST_END_LAST": "to last", "LISTS_GET_SUBLIST_TAIL": "", "LISTS_GET_SUBLIST_TOOLTIP": "Creates a copy of the specified portion of a list.", - "LISTS_SORT_HELPURL": "https://github.com/google/blockly/wiki/Lists#sorting-a-list", + "LISTS_SORT_HELPURL": "https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#sorting-a-list", "LISTS_SORT_TITLE": "sort %1 %2 %3", "LISTS_SORT_TOOLTIP": "Sort a copy of a list.", "LISTS_SORT_ORDER_ASCENDING": "ascending", @@ -348,20 +349,20 @@ "LISTS_SORT_TYPE_NUMERIC": "numeric", "LISTS_SORT_TYPE_TEXT": "alphabetic", "LISTS_SORT_TYPE_IGNORECASE": "alphabetic, ignore case", - "LISTS_SPLIT_HELPURL": "https://github.com/google/blockly/wiki/Lists#splitting-strings-and-joining-lists", + "LISTS_SPLIT_HELPURL": "https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#splitting-strings-and-joining-lists", "LISTS_SPLIT_LIST_FROM_TEXT": "make list from text", "LISTS_SPLIT_TEXT_FROM_LIST": "make text from list", "LISTS_SPLIT_WITH_DELIMITER": "with delimiter", "LISTS_SPLIT_TOOLTIP_SPLIT": "Split text into a list of texts, breaking at each delimiter.", "LISTS_SPLIT_TOOLTIP_JOIN": "Join a list of texts into one text, separated by a delimiter.", - "LISTS_REVERSE_HELPURL": "https://github.com/google/blockly/wiki/Lists#reversing-a-list", + "LISTS_REVERSE_HELPURL": "https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#reversing-a-list", "LISTS_REVERSE_MESSAGE0": "reverse %1", "LISTS_REVERSE_TOOLTIP": "Reverse a copy of a list.", "ORDINAL_NUMBER_SUFFIX": "", - "VARIABLES_GET_HELPURL": "https://github.com/google/blockly/wiki/Variables#get", + "VARIABLES_GET_HELPURL": "https://github.com/RaspberryPiFoundation/blockly/wiki/Variables#get", "VARIABLES_GET_TOOLTIP": "Returns the value of this variable.", "VARIABLES_GET_CREATE_SET": "Create 'set %1'", - "VARIABLES_SET_HELPURL": "https://github.com/google/blockly/wiki/Variables#set", + "VARIABLES_SET_HELPURL": "https://github.com/RaspberryPiFoundation/blockly/wiki/Variables#set", "VARIABLES_SET": "set %1 to %2", "VARIABLES_SET_TOOLTIP": "Sets this variable to be equal to the input.", "VARIABLES_SET_CREATE_GET": "Create 'get %1'", @@ -396,5 +397,27 @@ "WORKSPACE_ARIA_LABEL": "Blockly Workspace", "COLLAPSED_WARNINGS_WARNING": "Collapsed blocks contain warnings.", "DIALOG_OK": "OK", - "DIALOG_CANCEL": "Cancel" + "DIALOG_CANCEL": "Cancel", + "EDIT_BLOCK_CONTENTS": "Edit Block contents", + "MOVE_BLOCK": "Move Block", + "WINDOWS": "Windows", + "MAC_OS": "macOS", + "CHROME_OS": "ChromeOS", + "LINUX": "Linux", + "UNKNOWN": "Unknown", + "CONTROL_KEY": "Ctrl", + "COMMAND_KEY": "⌘ Command", + "OPTION_KEY": "⌥ Option", + "ALT_KEY": "Alt", + "CUT_SHORTCUT": "Cut", + "COPY_SHORTCUT": "Copy", + "PASTE_SHORTCUT": "Paste", + "HELP_PROMPT": "Press %1 for help on keyboard controls", + "SHORTCUTS_GENERAL": "General", + "SHORTCUTS_EDITING": "Editing", + "SHORTCUTS_CODE_NAVIGATION": "Code navigation", + "KEYBOARD_NAV_UNCONSTRAINED_MOVE_HINT": "Hold %1 and use arrow keys to move freely, then %2 to accept the position", + "KEYBOARD_NAV_CONSTRAINED_MOVE_HINT": "Use the arrow keys to move, then %1 to accept the position", + "KEYBOARD_NAV_COPIED_HINT": "Copied. Press %1 to paste.", + "KEYBOARD_NAV_CUT_HINT": "Cut. Press %1 to paste." } diff --git a/msg/json/eo.json b/packages/blockly/msg/json/eo.json similarity index 100% rename from msg/json/eo.json rename to packages/blockly/msg/json/eo.json diff --git a/msg/json/es.json b/packages/blockly/msg/json/es.json similarity index 99% rename from msg/json/es.json rename to packages/blockly/msg/json/es.json index 66118d46dad..309750f3f13 100644 --- a/msg/json/es.json +++ b/packages/blockly/msg/json/es.json @@ -7,7 +7,9 @@ "Eulalio", "Fitoschido", "Harvest", + "Ignatgg", "Indiralena", + "Josuert", "Julián L", "Ktranz", "Luisangelrg", @@ -332,6 +334,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "hacer algo", "PROCEDURES_BEFORE_PARAMS": "con:", "PROCEDURES_CALL_BEFORE_PARAMS": "con:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "No se puede ejecutar la función definida por el usuario '%1' porque el bloque de definición está deshabilitado.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Crea una función sin salida.", "PROCEDURES_DEFNORETURN_COMMENT": "Describe esta función...", "PROCEDURES_DEFRETURN_RETURN": "devuelve", diff --git a/msg/json/et.json b/packages/blockly/msg/json/et.json similarity index 98% rename from msg/json/et.json rename to packages/blockly/msg/json/et.json index 91af281c567..16f4d0e00cb 100644 --- a/msg/json/et.json +++ b/packages/blockly/msg/json/et.json @@ -5,6 +5,7 @@ "Hasso", "Ilmarine", "Masavi", + "Pajusmar", "Puik" ] }, @@ -41,6 +42,7 @@ "NEW_VARIABLE_TITLE": "Uue muutuja nimi:", "VARIABLE_ALREADY_EXISTS": "'%1'-nimeline muutuja on juba olemas.", "VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE": "Muutuja nimega '%1' juba eksisteerib teise tüübina: '%2'.", + "VARIABLE_ALREADY_EXISTS_FOR_A_PARAMETER": "Muutuja nimega '%1' on protseduuris '%2' juba parameetrina olemas.", "DELETE_VARIABLE_CONFIRMATION": "Kas kustutada %1 kohas kasutatav muutuja '%2'?", "CANNOT_DELETE_VARIABLE_PROCEDURE": "Muutujat '%1' ei saa kustutada, sest see on osa funktsiooni '%2' määratlusest", "DELETE_VARIABLE": "Kustuta muutuja '%1'", @@ -106,6 +108,12 @@ "LOGIC_TERNARY_TOOLTIP": "Kui tingimuse väärtus on tõene, tagastab „kui tõene“ väärtuse, vastasel juhul „kui väär“ väärtuse.", "MATH_NUMBER_HELPURL": "https://et.wikipedia.org/wiki/Arv", "MATH_NUMBER_TOOLTIP": "Arv.", + "MATH_TRIG_SIN": "sin", + "MATH_TRIG_COS": "cos", + "MATH_TRIG_TAN": "tan", + "MATH_TRIG_ASIN": "asin", + "MATH_TRIG_ACOS": "acos", + "MATH_TRIG_ATAN": "atan", "MATH_ARITHMETIC_HELPURL": "https://et.wikipedia.org/wiki/Aritmeetika", "MATH_ARITHMETIC_TOOLTIP_ADD": "Tagastab kahe arvu summa.", "MATH_ARITHMETIC_TOOLTIP_MINUS": "Tagastab kahe arvu vahe.", @@ -300,6 +308,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "teeme midagi", "PROCEDURES_BEFORE_PARAMS": "sisenditega:", "PROCEDURES_CALL_BEFORE_PARAMS": "sisenditega:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Kasutaja määratud funktsiooni '%1' ei saa käivitada, kuna määratlusplokk on keelatud.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Tekitab funktsiooni, mis ei tagasta midagi.", "PROCEDURES_DEFNORETURN_COMMENT": "Funktsiooni kirjeldus ...", "PROCEDURES_DEFRETURN_RETURN": "tagasta", diff --git a/msg/json/eu.json b/packages/blockly/msg/json/eu.json similarity index 100% rename from msg/json/eu.json rename to packages/blockly/msg/json/eu.json diff --git a/msg/json/fa.json b/packages/blockly/msg/json/fa.json similarity index 99% rename from msg/json/fa.json rename to packages/blockly/msg/json/fa.json index a27d412ab45..d839853165e 100644 --- a/msg/json/fa.json +++ b/packages/blockly/msg/json/fa.json @@ -7,6 +7,7 @@ "AzorAhai", "Dalba", "Darafsh", + "Ebrahim", "Ebraminio", "Hamisun", "Hossein.safavi", diff --git a/msg/json/fi.json b/packages/blockly/msg/json/fi.json similarity index 100% rename from msg/json/fi.json rename to packages/blockly/msg/json/fi.json diff --git a/msg/json/fo.json b/packages/blockly/msg/json/fo.json similarity index 100% rename from msg/json/fo.json rename to packages/blockly/msg/json/fo.json diff --git a/msg/json/fr.json b/packages/blockly/msg/json/fr.json similarity index 98% rename from msg/json/fr.json rename to packages/blockly/msg/json/fr.json index 4d6b9894ca6..eadac77e7ec 100644 --- a/msg/json/fr.json +++ b/packages/blockly/msg/json/fr.json @@ -84,7 +84,7 @@ "CONTROLS_WHILEUNTIL_TOOLTIP_WHILE": "Tant qu’une valeur est vraie, alors exécuter des instructions.", "CONTROLS_WHILEUNTIL_TOOLTIP_UNTIL": "Tant qu’une valeur est fausse, alors exécuter des instructions.", "CONTROLS_FOR_HELPURL": "https://fr.wikipedia.org/wiki/Boucle_for", - "CONTROLS_FOR_TOOLTIP": "Faire prendre successivement à la variable « %1 » les valeurs entre deux nombres de début et de fin par incrément du pas spécifié et exécuter les instructions spécifiées.", + "CONTROLS_FOR_TOOLTIP": "Faire prendre successivement à la variable « %1 » les valeurs entre le nombre du début et celui de fin en incrémentant du pas spécifié et exécuter les instructions spécifiées.", "CONTROLS_FOR_TITLE": "compter avec %1 de %2 à %3 par %4", "CONTROLS_FOREACH_HELPURL": "https://fr.wikipedia.org/wiki/Structure_de_contrôle#Itérateurs", "CONTROLS_FOREACH_TITLE": "pour chaque élément %1 dans la liste %2", @@ -346,6 +346,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "faire quelque chose", "PROCEDURES_BEFORE_PARAMS": "avec :", "PROCEDURES_CALL_BEFORE_PARAMS": "avec :", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Impossible d'exécuter la fonction définie par l'utilisateur '%1' car le bloc de définition est désactivé.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Crée une fonction sans sortie.", "PROCEDURES_DEFNORETURN_COMMENT": "Décrivez cette fonction...", "PROCEDURES_DEFRETURN_HELPURL": "https://fr.wikipedia.org/wiki/Sous-programme", diff --git a/msg/json/frr.json b/packages/blockly/msg/json/frr.json similarity index 100% rename from msg/json/frr.json rename to packages/blockly/msg/json/frr.json diff --git a/msg/json/gl.json b/packages/blockly/msg/json/gl.json similarity index 100% rename from msg/json/gl.json rename to packages/blockly/msg/json/gl.json diff --git a/msg/json/gn.json b/packages/blockly/msg/json/gn.json similarity index 100% rename from msg/json/gn.json rename to packages/blockly/msg/json/gn.json diff --git a/msg/json/gor.json b/packages/blockly/msg/json/gor.json similarity index 100% rename from msg/json/gor.json rename to packages/blockly/msg/json/gor.json diff --git a/msg/json/ha.json b/packages/blockly/msg/json/ha.json similarity index 100% rename from msg/json/ha.json rename to packages/blockly/msg/json/ha.json diff --git a/msg/json/hak.json b/packages/blockly/msg/json/hak.json similarity index 100% rename from msg/json/hak.json rename to packages/blockly/msg/json/hak.json diff --git a/msg/json/he.json b/packages/blockly/msg/json/he.json similarity index 99% rename from msg/json/he.json rename to packages/blockly/msg/json/he.json index c5db9108e3d..954a9f03f00 100644 --- a/msg/json/he.json +++ b/packages/blockly/msg/json/he.json @@ -64,7 +64,7 @@ "COLOUR_RGB_RED": "אדום", "COLOUR_RGB_GREEN": "ירוק", "COLOUR_RGB_BLUE": "כחול", - "COLOUR_RGB_TOOLTIP": "צור צבע עם הסכום המצוין של אדום, ירוק וכחול. כל הערכים חייבים להיות בין 0 ל100.", + "COLOUR_RGB_TOOLTIP": "צור צבע עם הסכום המצוין של אדום, ירוק וכחול. כל הערכים חייבים להיות בין 0 ל־100.", "COLOUR_BLEND_TITLE": "ערבב", "COLOUR_BLEND_COLOUR1": "צבע 1", "COLOUR_BLEND_COLOUR2": "צבע 2", @@ -328,6 +328,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "לעשות משהו", "PROCEDURES_BEFORE_PARAMS": "עם:", "PROCEDURES_CALL_BEFORE_PARAMS": "עם:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "אי־אפשר להריץ את הפונקציה „%1” שהוגדרה על־ידי המשתמש כיוון שמקטע ההגדרה מושבת.", "PROCEDURES_DEFNORETURN_TOOLTIP": "יצירת פונקציה ללא פלט.", "PROCEDURES_DEFNORETURN_COMMENT": "תאר את הפונקציה הזאת...", "PROCEDURES_DEFRETURN_RETURN": "להחזיר", diff --git a/msg/json/hi.json b/packages/blockly/msg/json/hi.json similarity index 100% rename from msg/json/hi.json rename to packages/blockly/msg/json/hi.json diff --git a/msg/json/hr.json b/packages/blockly/msg/json/hr.json similarity index 97% rename from msg/json/hr.json rename to packages/blockly/msg/json/hr.json index 500f8395af8..22831784578 100644 --- a/msg/json/hr.json +++ b/packages/blockly/msg/json/hr.json @@ -4,6 +4,7 @@ "Bugoslav", "Gordana Sokol", "Lkralj15", + "N0one", "Ninocka", "Npavcec", "Tjagust" @@ -42,6 +43,7 @@ "NEW_VARIABLE_TITLE": "Ime nove varijable:", "VARIABLE_ALREADY_EXISTS": "Varijabla s nazivom '%1' već postoji.", "VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE": "Varijabla pod nazivom '%1' već postoji za drugi tip: '%2'", + "VARIABLE_ALREADY_EXISTS_FOR_A_PARAMETER": "Varijabla pod nazivom '%1' već postoji kao parametar u proceduri '%2'.", "DELETE_VARIABLE_CONFIRMATION": "Obriši %1 korištenja varijable '%2'?", "CANNOT_DELETE_VARIABLE_PROCEDURE": "Ne mogu obrisati varijablu '%1' zato što je dio definicije funkcije '%2'", "DELETE_VARIABLE": "Obriši varijablu '%1'", @@ -109,6 +111,12 @@ "LOGIC_TERNARY_TOOLTIP": "Provjerite uvjet u \"izrazu\". Ako je uvjet istinit, vraća vrijednost \"ako je istinito\"; inače vraća vrijednost \"ako je lažno\".", "MATH_NUMBER_HELPURL": "https://hr.wikipedia.org/wiki/Broj", "MATH_NUMBER_TOOLTIP": "broj", + "MATH_TRIG_SIN": "sin", + "MATH_TRIG_COS": "cos", + "MATH_TRIG_TAN": "tan", + "MATH_TRIG_ASIN": "asin", + "MATH_TRIG_ACOS": "acos", + "MATH_TRIG_ATAN": "atan", "MATH_ARITHMETIC_HELPURL": "https://hr.wikipedia.org/wiki/Aritmetika", "MATH_ARITHMETIC_TOOLTIP_ADD": "Vraća zbroj dvaju brojeva.", "MATH_ARITHMETIC_TOOLTIP_MINUS": "Vraća razliku dvaju brojeva.", @@ -244,6 +252,7 @@ "LISTS_GET_INDEX_GET": "dohvati", "LISTS_GET_INDEX_GET_REMOVE": "uzmi i ukloni", "LISTS_GET_INDEX_REMOVE": "ukloni", + "LISTS_GET_INDEX_FROM_START": "#", "LISTS_GET_INDEX_FROM_END": "# od kraja", "LISTS_GET_INDEX_FIRST": "prvi", "LISTS_GET_INDEX_LAST": "posljednji", @@ -303,6 +312,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "napravi nešto", "PROCEDURES_BEFORE_PARAMS": "s:", "PROCEDURES_CALL_BEFORE_PARAMS": "s:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Nije moguće pokrenuti korisnički definiranu funkciju '%1' jer je blok definicije onemogućen.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Stvaranje funkcije bez izlazne vrijednosti", "PROCEDURES_DEFNORETURN_COMMENT": "Opis funkcije", "PROCEDURES_DEFRETURN_RETURN": "Vrati", diff --git a/msg/json/hrx.json b/packages/blockly/msg/json/hrx.json similarity index 100% rename from msg/json/hrx.json rename to packages/blockly/msg/json/hrx.json diff --git a/msg/json/hsb.json b/packages/blockly/msg/json/hsb.json similarity index 100% rename from msg/json/hsb.json rename to packages/blockly/msg/json/hsb.json diff --git a/msg/json/hu.json b/packages/blockly/msg/json/hu.json similarity index 100% rename from msg/json/hu.json rename to packages/blockly/msg/json/hu.json diff --git a/msg/json/hy.json b/packages/blockly/msg/json/hy.json similarity index 100% rename from msg/json/hy.json rename to packages/blockly/msg/json/hy.json diff --git a/msg/json/ia.json b/packages/blockly/msg/json/ia.json similarity index 99% rename from msg/json/ia.json rename to packages/blockly/msg/json/ia.json index f7600660271..6cc26f25d53 100644 --- a/msg/json/ia.json +++ b/packages/blockly/msg/json/ia.json @@ -14,8 +14,8 @@ "ADD_COMMENT": "Adder commento", "REMOVE_COMMENT": "Remover commento", "DUPLICATE_COMMENT": "Duplicar commento", - "EXTERNAL_INPUTS": "Entrata externe", - "INLINE_INPUTS": "Entrata interne", + "EXTERNAL_INPUTS": "Entratas externe", + "INLINE_INPUTS": "Entratas interne", "DELETE_BLOCK": "Deler bloco", "DELETE_X_BLOCKS": "Deler %1 blocos", "DELETE_ALL_BLOCKS": "Deler tote le %1 blocos?", diff --git a/msg/json/id.json b/packages/blockly/msg/json/id.json similarity index 98% rename from msg/json/id.json rename to packages/blockly/msg/json/id.json index bea468f98af..49da77c08a2 100644 --- a/msg/json/id.json +++ b/packages/blockly/msg/json/id.json @@ -5,10 +5,12 @@ "Akmaie Ajam", "Arifin.wijaya", "Daud I.F. Argana", + "ID Owly01", "Kasimtan", "Kenrick95", "Marwan Mohamad", "Mirws", + "NikolasKHF", "PutriAmalia1991", "Veracious", "아라" @@ -22,7 +24,7 @@ "REMOVE_COMMENT": "Hapus Komentar", "DUPLICATE_COMMENT": "Gandakan Komentar", "EXTERNAL_INPUTS": "Input Eksternal", - "INLINE_INPUTS": "Input Inline", + "INLINE_INPUTS": "Input Sebaris", "DELETE_BLOCK": "Hapus Blok", "DELETE_X_BLOCKS": "Hapus %1 Blok", "DELETE_ALL_BLOCKS": "Hapus semua %1 blok?", @@ -41,7 +43,7 @@ "RENAME_VARIABLE_TITLE": "Ubah nama semua variabel '%1' menjadi:", "NEW_VARIABLE": "Buat variabel...", "NEW_STRING_VARIABLE": "Buat variabel string...", - "NEW_NUMBER_VARIABLE": "Buat variabel number...", + "NEW_NUMBER_VARIABLE": "Buat variabel bilangan...", "NEW_COLOUR_VARIABLE": "Buat variabel warna...", "NEW_VARIABLE_TYPE_TITLE": "Tipe variabel baru:", "NEW_VARIABLE_TITLE": "Nama variabel baru:", @@ -311,6 +313,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "buat sesuatu", "PROCEDURES_BEFORE_PARAMS": "dengan:", "PROCEDURES_CALL_BEFORE_PARAMS": "dengan:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Tidak dapat menjalankan fungsi yang ditentukan pengguna '%1' karena blok definisi dinonaktifkan.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Buat sebuah fungsi tanpa output.", "PROCEDURES_DEFNORETURN_COMMENT": "Jelaskan fungsi ini...", "PROCEDURES_DEFRETURN_RETURN": "kembali", diff --git a/msg/json/ig.json b/packages/blockly/msg/json/ig.json similarity index 100% rename from msg/json/ig.json rename to packages/blockly/msg/json/ig.json diff --git a/msg/json/inh.json b/packages/blockly/msg/json/inh.json similarity index 100% rename from msg/json/inh.json rename to packages/blockly/msg/json/inh.json diff --git a/msg/json/is.json b/packages/blockly/msg/json/is.json similarity index 100% rename from msg/json/is.json rename to packages/blockly/msg/json/is.json diff --git a/msg/json/it.json b/packages/blockly/msg/json/it.json similarity index 100% rename from msg/json/it.json rename to packages/blockly/msg/json/it.json diff --git a/msg/json/ja.json b/packages/blockly/msg/json/ja.json similarity index 99% rename from msg/json/ja.json rename to packages/blockly/msg/json/ja.json index 0cee86da959..3fcd1bd844f 100644 --- a/msg/json/ja.json +++ b/packages/blockly/msg/json/ja.json @@ -335,6 +335,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "何かする", "PROCEDURES_BEFORE_PARAMS": "引数:", "PROCEDURES_CALL_BEFORE_PARAMS": "引数:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "定義ブロックが無効のため、ユーザー定義関数 '%1' を実行できません。", "PROCEDURES_DEFNORETURN_TOOLTIP": "出力なしの関数を作成します。", "PROCEDURES_DEFNORETURN_COMMENT": "この関数の説明…", "PROCEDURES_DEFRETURN_RETURN": "返す", diff --git a/msg/json/ka.json b/packages/blockly/msg/json/ka.json similarity index 100% rename from msg/json/ka.json rename to packages/blockly/msg/json/ka.json diff --git a/msg/json/kab.json b/packages/blockly/msg/json/kab.json similarity index 100% rename from msg/json/kab.json rename to packages/blockly/msg/json/kab.json diff --git a/msg/json/kbd-cyrl.json b/packages/blockly/msg/json/kbd-cyrl.json similarity index 100% rename from msg/json/kbd-cyrl.json rename to packages/blockly/msg/json/kbd-cyrl.json diff --git a/msg/json/km.json b/packages/blockly/msg/json/km.json similarity index 100% rename from msg/json/km.json rename to packages/blockly/msg/json/km.json diff --git a/msg/json/kn.json b/packages/blockly/msg/json/kn.json similarity index 99% rename from msg/json/kn.json rename to packages/blockly/msg/json/kn.json index b811ca90c0e..1c1cdaf819e 100644 --- a/msg/json/kn.json +++ b/packages/blockly/msg/json/kn.json @@ -3,6 +3,7 @@ "authors": [ "Ananth subray", "Anoop rao", + "Anzx", "Ashay vb", "Ksh31", "Ksramwiki1957", diff --git a/msg/json/ko.json b/packages/blockly/msg/json/ko.json similarity index 95% rename from msg/json/ko.json rename to packages/blockly/msg/json/ko.json index bbfa5eb9488..142143422c6 100644 --- a/msg/json/ko.json +++ b/packages/blockly/msg/json/ko.json @@ -2,7 +2,6 @@ "@metadata": { "authors": [ "Alex00728", - "Amire80", "Codenstory", "Delanoor", "Dr1t jg", @@ -21,6 +20,7 @@ "SeoJeongHo", "Snddl3", "Suleiman the Magnificent Television", + "YeBoy371", "Ykhwong", "아라" ] @@ -80,18 +80,18 @@ "CONTROLS_REPEAT_TITLE": "%1회 반복", "CONTROLS_REPEAT_INPUT_DO": "하기", "CONTROLS_REPEAT_TOOLTIP": "여러 번 반복해 명령들을 실행합니다.", - "CONTROLS_WHILEUNTIL_HELPURL": "https://ko.wikipedia.org/wiki/While_%EB%A3%A8%ED%94%84", + "CONTROLS_WHILEUNTIL_HELPURL": "https://github.com/google/blockly/wiki/Loops#repeat", "CONTROLS_WHILEUNTIL_OPERATOR_WHILE": "동안 반복", "CONTROLS_WHILEUNTIL_OPERATOR_UNTIL": "다음까지 반복", "CONTROLS_WHILEUNTIL_TOOLTIP_WHILE": "값이 참일 때, 명령들을 실행합니다.", "CONTROLS_WHILEUNTIL_TOOLTIP_UNTIL": "값이 거짓인 동안 명령문을 실행합니다.", - "CONTROLS_FOR_HELPURL": "https://ko.wikipedia.org/wiki/For_%EB%A3%A8%ED%94%84", + "CONTROLS_FOR_HELPURL": "https://github.com/google/blockly/wiki/Loops#count-with", "CONTROLS_FOR_TOOLTIP": "변수 '%1'이(가) 지정된 간격으로 시작 번호에서 끝 번호까지 세는 동시에 지정된 블록을 실행하게 하세요.", "CONTROLS_FOR_TITLE": "으로 계산 %1 %2에서 %4을 이용하여 %3로", - "CONTROLS_FOREACH_HELPURL": "https://ko.wikipedia.org/wiki/For_%EB%A3%A8%ED%94%84#.EC.9E.84.EC.9D.98.EC.9D.98_.EC.A7.91.ED.95.A9", + "CONTROLS_FOREACH_HELPURL": "https://github.com/google/blockly/wiki/Loops#for-each", "CONTROLS_FOREACH_TITLE": "각 항목에 대해 %1 목록으로 %2", "CONTROLS_FOREACH_TOOLTIP": "리스트 안에 들어있는 각 아이템들을, 순서대로 변수 '%1' 에 한 번씩 저장시키고, 그 때 마다 명령을 실행합니다.", - "CONTROLS_FLOW_STATEMENTS_HELPURL": "https://ko.wikipedia.org/wiki/%EC%A0%9C%EC%96%B4_%ED%9D%90%EB%A6%84", + "CONTROLS_FLOW_STATEMENTS_HELPURL": "https://github.com/google/blockly/wiki/Loops#loop-termination-blocks", "CONTROLS_FLOW_STATEMENTS_OPERATOR_BREAK": "반복 중단", "CONTROLS_FLOW_STATEMENTS_OPERATOR_CONTINUE": "다음 반복", "CONTROLS_FLOW_STATEMENTS_TOOLTIP_BREAK": "현재 반복 실행 블럭을 빠져나갑니다.", @@ -115,21 +115,22 @@ "LOGIC_COMPARE_TOOLTIP_LTE": "첫 번째 값이 두 번째 값보다 작거나 같으면, 참(true) 값을 돌려줍니다.", "LOGIC_COMPARE_TOOLTIP_GT": "첫 번째 값이 두 번째 값보다 크면, 참(true) 값을 돌려줍니다.", "LOGIC_COMPARE_TOOLTIP_GTE": "첫 번째 값이 두 번째 값보다 크거나 같으면, 참(true) 값을 돌려줍니다.", - "LOGIC_OPERATION_HELPURL": "https://ko.wikipedia.org/wiki/%EB%B6%88_%EB%85%BC%EB%A6%AC", + "LOGIC_OPERATION_HELPURL": "https://github.com/google/blockly/wiki/Logic#logical-operations", "LOGIC_OPERATION_TOOLTIP_AND": "두 값이 모두 참(true) 값이면, 참 값을 돌려줍니다.", "LOGIC_OPERATION_AND": "그리고", "LOGIC_OPERATION_TOOLTIP_OR": "적어도 하나의 값이 참일 경우 참을 반환합니다.", "LOGIC_OPERATION_OR": "또는", - "LOGIC_NEGATE_HELPURL": "https://ko.wikipedia.org/wiki/%EB%B6%80%EC%A0%95", + "LOGIC_NEGATE_HELPURL": "https://github.com/google/blockly/wiki/Logic#not", "LOGIC_NEGATE_TITLE": "%1가 아닙니다", "LOGIC_NEGATE_TOOLTIP": "입력값이 거짓이라면 참을 반환합니다. 참이라면 거짓을 반환합니다.", - "LOGIC_BOOLEAN_HELPURL": "https://ko.wikipedia.org/wiki/%EC%A7%84%EB%A6%BF%EA%B0%92", + "LOGIC_BOOLEAN_HELPURL": "https://github.com/google/blockly/wiki/Logic#values", "LOGIC_BOOLEAN_TRUE": "참", "LOGIC_BOOLEAN_FALSE": "거짓", "LOGIC_BOOLEAN_TOOLTIP": "참 혹은 거짓 모두 반환합니다.", + "LOGIC_NULL_HELPURL": "https://ko.wikipedia.org/wiki/널러블_타입", "LOGIC_NULL": "빈 값", "LOGIC_NULL_TOOLTIP": "빈 값을 반환합니다.", - "LOGIC_TERNARY_HELPURL": "https://ko.wikipedia.org/wiki/물음표", + "LOGIC_TERNARY_HELPURL": "https://ko.wikipedia.org/wiki/%3F:", "LOGIC_TERNARY_CONDITION": "테스트", "LOGIC_TERNARY_IF_TRUE": "만약 참이라면", "LOGIC_TERNARY_IF_FALSE": "만약 거짓이라면", @@ -199,13 +200,16 @@ "MATH_ONLIST_TOOLTIP_STD_DEV": "이 리스트의 표준 편차를 반환합니다.", "MATH_ONLIST_OPERATOR_RANDOM": "목록의 임의 항목", "MATH_ONLIST_TOOLTIP_RANDOM": "목록에서 임의의 아이템을 돌려줍니다.", + "MATH_MODULO_HELPURL": "https://ko.wikipedia.org/wiki/모듈로", "MATH_MODULO_TITLE": "%1 ÷ %2의 나머지", "MATH_MODULO_TOOLTIP": "첫 번째 수를 두 번째 수로 나눈, 나머지 값을 돌려줍니다.", "MATH_CONSTRAIN_HELPURL": "https://ko.wikipedia.org/wiki/클램핑_(그래픽)", "MATH_CONSTRAIN_TITLE": "%1의 값을, 최소 %2 최대 %3으로 조정", "MATH_CONSTRAIN_TOOLTIP": "어떤 수를, 특정 범위의 값이 되도록 강제로 조정합니다.", + "MATH_RANDOM_INT_HELPURL": "https://ko.wikipedia.org/wiki/난수 발생기", "MATH_RANDOM_INT_TITLE": "랜덤정수(%1<= n <=%2)", "MATH_RANDOM_INT_TOOLTIP": "두 주어진 제한된 범위 사이의 임의 정수값을 돌려줍니다.", + "MATH_RANDOM_FLOAT_HELPURL": "https://ko.wikipedia.org/wiki/난수 발생기", "MATH_RANDOM_FLOAT_TITLE_RANDOM": "임의 분수", "MATH_RANDOM_FLOAT_TOOLTIP": "0.0 (포함)과 1.0 (배타적) 사이의 임의 분수 값을 돌려줍니다.", "MATH_ATAN2_TITLE": "X:%1 Y:%2의 atan2", @@ -333,13 +337,14 @@ "LISTS_SPLIT_TOOLTIP_JOIN": "구분 기호로 구분하여 텍스트 목록을 하나의 텍스트에 병합합니다.", "LISTS_REVERSE_MESSAGE0": "%1 뒤집기", "LISTS_REVERSE_TOOLTIP": "리스트의 복사본을 뒤집습니다.", - "VARIABLES_GET_HELPURL": "https://ko.wikipedia.org/wiki/%EB%B3%80%EC%88%98_(%EC%BB%B4%ED%93%A8%ED%84%B0_%EA%B3%BC%ED%95%99)", + "VARIABLES_GET_HELPURL": "https://github.com/google/blockly/wiki/Variables#get", "VARIABLES_GET_TOOLTIP": "변수에 저장 되어있는 값을 돌려줍니다.", "VARIABLES_GET_CREATE_SET": "'집합 %1' 생성", - "VARIABLES_SET_HELPURL": "https://ko.wikipedia.org/wiki/%EB%B3%80%EC%88%98_(%EC%BB%B4%ED%93%A8%ED%84%B0_%EA%B3%BC%ED%95%99)", + "VARIABLES_SET_HELPURL": "https://github.com/google/blockly/wiki/Variables#set", "VARIABLES_SET": "%1를 %2로 설정", "VARIABLES_SET_TOOLTIP": "변수의 값을 입력한 값으로 변경해 줍니다.", "VARIABLES_SET_CREATE_GET": "'%1 값 읽기' 블럭 생성", + "PROCEDURES_DEFNORETURN_HELPURL": "https://ko.wikipedia.org/wiki/함수_(컴퓨터_과학)", "PROCEDURES_DEFNORETURN_TITLE": "함수", "PROCEDURES_DEFNORETURN_PROCEDURE": "함수 이름", "PROCEDURES_BEFORE_PARAMS": "사용:", diff --git a/msg/json/ksh.json b/packages/blockly/msg/json/ksh.json similarity index 100% rename from msg/json/ksh.json rename to packages/blockly/msg/json/ksh.json diff --git a/msg/json/ku-latn.json b/packages/blockly/msg/json/ku-latn.json similarity index 100% rename from msg/json/ku-latn.json rename to packages/blockly/msg/json/ku-latn.json diff --git a/msg/json/ky.json b/packages/blockly/msg/json/ky.json similarity index 100% rename from msg/json/ky.json rename to packages/blockly/msg/json/ky.json diff --git a/msg/json/la.json b/packages/blockly/msg/json/la.json similarity index 100% rename from msg/json/la.json rename to packages/blockly/msg/json/la.json diff --git a/msg/json/lb.json b/packages/blockly/msg/json/lb.json similarity index 99% rename from msg/json/lb.json rename to packages/blockly/msg/json/lb.json index e373761e652..900ce30a23d 100644 --- a/msg/json/lb.json +++ b/packages/blockly/msg/json/lb.json @@ -55,7 +55,7 @@ "CONTROLS_REPEAT_INPUT_DO": "maach", "CONTROLS_WHILEUNTIL_OPERATOR_WHILE": "Widderhuel soulaang", "CONTROLS_WHILEUNTIL_OPERATOR_UNTIL": "widderhuele bis", - "CONTROLS_WHILEUNTIL_TOOLTIP_WHILE": "Féiert d'Uweisungen aus, soulaang wéi de Wäert richteg ass", + "CONTROLS_WHILEUNTIL_TOOLTIP_WHILE": "Féiert d'Uweisungen aus, soulaang wéi de Wäert wouer ass.", "CONTROLS_WHILEUNTIL_TOOLTIP_UNTIL": "Féiert d'Uweisungen aus, soulaang wéi de Wäert falsch ass.", "CONTROLS_FOR_TITLE": "zielt mat %1 vun %2 bis %3 mat %4", "CONTROLS_FOREACH_TITLE": "fir all Element %1 an der Lëscht %2", diff --git a/msg/json/lki.json b/packages/blockly/msg/json/lki.json similarity index 100% rename from msg/json/lki.json rename to packages/blockly/msg/json/lki.json diff --git a/msg/json/lo.json b/packages/blockly/msg/json/lo.json similarity index 100% rename from msg/json/lo.json rename to packages/blockly/msg/json/lo.json diff --git a/msg/json/lrc.json b/packages/blockly/msg/json/lrc.json similarity index 100% rename from msg/json/lrc.json rename to packages/blockly/msg/json/lrc.json diff --git a/msg/json/lt.json b/packages/blockly/msg/json/lt.json similarity index 100% rename from msg/json/lt.json rename to packages/blockly/msg/json/lt.json diff --git a/msg/json/lv.json b/packages/blockly/msg/json/lv.json similarity index 100% rename from msg/json/lv.json rename to packages/blockly/msg/json/lv.json diff --git a/msg/json/mg.json b/packages/blockly/msg/json/mg.json similarity index 100% rename from msg/json/mg.json rename to packages/blockly/msg/json/mg.json diff --git a/msg/json/mk.json b/packages/blockly/msg/json/mk.json similarity index 100% rename from msg/json/mk.json rename to packages/blockly/msg/json/mk.json diff --git a/msg/json/ml.json b/packages/blockly/msg/json/ml.json similarity index 100% rename from msg/json/ml.json rename to packages/blockly/msg/json/ml.json diff --git a/msg/json/mnw.json b/packages/blockly/msg/json/mnw.json similarity index 100% rename from msg/json/mnw.json rename to packages/blockly/msg/json/mnw.json diff --git a/msg/json/ms.json b/packages/blockly/msg/json/ms.json similarity index 100% rename from msg/json/ms.json rename to packages/blockly/msg/json/ms.json diff --git a/msg/json/my.json b/packages/blockly/msg/json/my.json similarity index 100% rename from msg/json/my.json rename to packages/blockly/msg/json/my.json diff --git a/msg/json/mzn.json b/packages/blockly/msg/json/mzn.json similarity index 100% rename from msg/json/mzn.json rename to packages/blockly/msg/json/mzn.json diff --git a/msg/json/nb.json b/packages/blockly/msg/json/nb.json similarity index 99% rename from msg/json/nb.json rename to packages/blockly/msg/json/nb.json index 55a59c8a584..9b5d3773c57 100644 --- a/msg/json/nb.json +++ b/packages/blockly/msg/json/nb.json @@ -306,6 +306,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "gjør noe", "PROCEDURES_BEFORE_PARAMS": "med:", "PROCEDURES_CALL_BEFORE_PARAMS": "med:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Kan ikke kjøre den brukerdefinerte funksjonen «%1» fordi definisjonsblokken er deaktivert.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Opprett en funksjon som ikke har noe resultat.", "PROCEDURES_DEFNORETURN_COMMENT": "Beskriv denne funksjonen…", "PROCEDURES_DEFRETURN_RETURN": "returner", diff --git a/msg/json/ne.json b/packages/blockly/msg/json/ne.json similarity index 100% rename from msg/json/ne.json rename to packages/blockly/msg/json/ne.json diff --git a/msg/json/nl.json b/packages/blockly/msg/json/nl.json similarity index 99% rename from msg/json/nl.json rename to packages/blockly/msg/json/nl.json index 914a8ef8ded..694a53ff6c7 100644 --- a/msg/json/nl.json +++ b/packages/blockly/msg/json/nl.json @@ -44,7 +44,7 @@ "REDO": "Opnieuw", "CHANGE_VALUE_TITLE": "Waarde wijzigen:", "RENAME_VARIABLE": "Variabele hernoemen...", - "RENAME_VARIABLE_TITLE": "Alle variabelen \"%1\" hernoemen naar:", + "RENAME_VARIABLE_TITLE": "Alle variabelen “%1” hernoemen tot:", "NEW_VARIABLE": "Variabele maken...", "NEW_STRING_VARIABLE": "Tekstvariabele maken...", "NEW_NUMBER_VARIABLE": "Numeriek variabele maken...", @@ -330,6 +330,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "doe iets", "PROCEDURES_BEFORE_PARAMS": "met:", "PROCEDURES_CALL_BEFORE_PARAMS": "met:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Kan de door de gebruiker gedefinieerde functie ‘%1’ niet uitvoeren omdat het definitieblok is uitgeschakeld.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Maakt een functie zonder uitvoer.", "PROCEDURES_DEFNORETURN_COMMENT": "Deze functie beschrijven...", "PROCEDURES_DEFRETURN_HELPURL": "https://nl.wikipedia.org/wiki/Subprogramma", diff --git a/msg/json/oc.json b/packages/blockly/msg/json/oc.json similarity index 100% rename from msg/json/oc.json rename to packages/blockly/msg/json/oc.json diff --git a/msg/json/olo.json b/packages/blockly/msg/json/olo.json similarity index 100% rename from msg/json/olo.json rename to packages/blockly/msg/json/olo.json diff --git a/msg/json/pa.json b/packages/blockly/msg/json/pa.json similarity index 82% rename from msg/json/pa.json rename to packages/blockly/msg/json/pa.json index e784ddc391b..e0ff05007ce 100644 --- a/msg/json/pa.json +++ b/packages/blockly/msg/json/pa.json @@ -2,6 +2,7 @@ "@metadata": { "authors": [ "AnupamM", + "Cabal", "Jimidar", "Tow", "ਪ੍ਰਚਾਰਕ" @@ -25,12 +26,13 @@ "DISABLE_BLOCK": "ਬਲਾਕ ਬੰਦ ਕਰੋ", "ENABLE_BLOCK": "ਬਲਾਕ ਚਾਲੂ ਕਰੋ", "HELP": "ਮਦਦ", - "UNDO": "ਅਣਕੀਤਾ ਕਰੋ", + "UNDO": "ਰੱਦ ਕਰੋ", "REDO": "ਮੁੜ ਕਰੋ", "CHANGE_VALUE_TITLE": "ਮੁੱਲ ਬਦਲੋ:", "COLOUR_PICKER_TOOLTIP": "ਰੰਗ-ਫੱਟੀ ਵਿੱਚੋਂ ਰੰਗ ਚੁਣੋ", "COLOUR_RANDOM_TITLE": "ਰਲ਼ਵਾਂ ਰੰਗ", "COLOUR_RANDOM_TOOLTIP": "ਰਲ਼ਵਾਂ ਰੰਗ ਚੁਣੋ", + "COLOUR_RGB_TITLE": "ਦੇ ਨਾਲ ਰੰਗ ਕਰੋ", "COLOUR_RGB_RED": "ਲਾਲ", "COLOUR_RGB_GREEN": "ਹਰਾ", "COLOUR_RGB_BLUE": "ਨੀਲਾ", @@ -39,10 +41,13 @@ "COLOUR_BLEND_COLOUR1": "ਰੰਗ 1", "COLOUR_BLEND_COLOUR2": "ਰੰਗ 2", "COLOUR_BLEND_RATIO": "ਅਨੁਪਾਤ", - "COLOUR_BLEND_TOOLTIP": "ਦਿੱਤੇ ਅਨੁਪਾਤ (0.0 - 1.0) ਅਨੁਸਾਰ ਦੋ ਰੰਗ ਮਿਲਾਓ।", + "COLOUR_BLEND_TOOLTIP": "ਦਿੱਤੇ ਅਨੁਪਾਤ (0.0 - 1.0) ਮੁਤਾਬਕ ਦੋ ਰੰਗ ਮਿਲਾਓ।", "CONTROLS_REPEAT_TITLE": "%1 ਵਾਰੀ ਦੁਹਰਾਉ", "CONTROLS_REPEAT_INPUT_DO": "ਕਰੋ", + "CONTROLS_WHILEUNTIL_OPERATOR_UNTIL": "ਦੁਹਰਾਓ ਜਦੋਂ ਤੱਕ", + "CONTROLS_FLOW_STATEMENTS_OPERATOR_BREAK": "ਘੇਰੇ ਚੋਂ ਬਾਹਰ ਕੱਢੋ", "CONTROLS_IF_MSG_IF": "ਜੇ", + "CONTROLS_IF_MSG_ELSEIF": "ਹੋਰ ਜੇ", "CONTROLS_IF_MSG_ELSE": "ਹੋਰ", "LOGIC_OPERATION_AND": "ਅਤੇ", "LOGIC_OPERATION_OR": "ਜਾਂ", @@ -50,7 +55,7 @@ "LOGIC_BOOLEAN_FALSE": "ਝੂਠ", "LOGIC_NULL": "ਨੱਲ", "LOGIC_NULL_TOOLTIP": "ਨੱਲ ਮੋੜਦਾ ਹੈ।", - "LOGIC_TERNARY_CONDITION": "ਟੈਸਟ", + "LOGIC_TERNARY_CONDITION": "ਪ੍ਰੀਖਿਆ", "LOGIC_TERNARY_IF_TRUE": "ਜੇ ਸੱਚ", "LOGIC_TERNARY_IF_FALSE": "ਜੇ ਝੂਠ", "MATH_NUMBER_TOOLTIP": "ਇੱਕ ਅੰਕ", @@ -60,9 +65,10 @@ "MATH_SINGLE_OP_ROOT": "ਵਰਗ ਮੂਲ", "MATH_SINGLE_TOOLTIP_ROOT": "ਇੱਕ ਅੰਕ ਦਾ ਵਰਗ ਮੂਲ ਮੋੜੋ।", "LISTS_GET_INDEX_FIRST": "ਪਹਿਲਾ", - "LISTS_GET_INDEX_LAST": "ਆਖ਼ਰੀ", + "LISTS_GET_INDEX_LAST": "ਆਖੀਰਲਾ", "LISTS_GET_INDEX_RANDOM": "ਰਲ਼ਵਾਂ", - "LISTS_SORT_ORDER_DESCENDING": "ਘਟਦੇ ਕ੍ਰਮ ਵਿੱਚ", + "LISTS_SORT_ORDER_ASCENDING": "ਚੜ੍ਹਦੀ ਤਰਤੀਬ ਵਿੱਚ", + "LISTS_SORT_ORDER_DESCENDING": "ਲਹਿੰਦੀ ਤਰਤੀਬ ਵਿੱਚ", "PROCEDURES_DEFRETURN_RETURN": "ਮੋੜੋ", "DIALOG_OK": "ਠੀਕ ਹੈ।", "DIALOG_CANCEL": "ਰੱਦ ਕਰੋ" diff --git a/msg/json/pl.json b/packages/blockly/msg/json/pl.json similarity index 99% rename from msg/json/pl.json rename to packages/blockly/msg/json/pl.json index c94d4875329..4d98e1e2cb2 100644 --- a/msg/json/pl.json +++ b/packages/blockly/msg/json/pl.json @@ -331,6 +331,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "zrób coś", "PROCEDURES_BEFORE_PARAMS": "z:", "PROCEDURES_CALL_BEFORE_PARAMS": "z:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Nie można uruchomić funkcji zdefiniowanej przez użytkownika '%1', ponieważ blok definicji jest wyłączony.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Tworzy funkcję nie posiadającą wyjścia.", "PROCEDURES_DEFNORETURN_COMMENT": "Opisz tę funkcję...", "PROCEDURES_DEFRETURN_RETURN": "zwróć", diff --git a/msg/json/pms.json b/packages/blockly/msg/json/pms.json similarity index 99% rename from msg/json/pms.json rename to packages/blockly/msg/json/pms.json index 0f54aa66533..99e92274cdc 100644 --- a/msg/json/pms.json +++ b/packages/blockly/msg/json/pms.json @@ -300,6 +300,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "fé cheicòs", "PROCEDURES_BEFORE_PARAMS": "con:", "PROCEDURES_CALL_BEFORE_PARAMS": "con:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Impossìbil fé marcé la fonsion definìa da l'utent '%1' përché ël blòch ëd definission a l'é disativà.", "PROCEDURES_DEFNORETURN_TOOLTIP": "A crea na fonsion sensa surtìa.", "PROCEDURES_DEFNORETURN_COMMENT": "Descrive sa fonsion...", "PROCEDURES_DEFRETURN_RETURN": "artorn", diff --git a/msg/json/ps.json b/packages/blockly/msg/json/ps.json similarity index 100% rename from msg/json/ps.json rename to packages/blockly/msg/json/ps.json diff --git a/msg/json/pt-br.json b/packages/blockly/msg/json/pt-br.json similarity index 99% rename from msg/json/pt-br.json rename to packages/blockly/msg/json/pt-br.json index 2754c603937..01fa44697d4 100644 --- a/msg/json/pt-br.json +++ b/packages/blockly/msg/json/pt-br.json @@ -17,6 +17,7 @@ "Lc97", "Lowvy", "Luk3", + "Maakhai", "Mauricio", "McDutchie", "Mordecaista", @@ -133,7 +134,7 @@ "LOGIC_TERNARY_TOOLTIP": "Avalia a condição em \"teste\". Se a condição for verdadeira retorna o valor \"se verdadeiro\", senão retorna o valor \"se falso\".", "MATH_NUMBER_HELPURL": "https://pt.wikipedia.org/wiki/N%C3%BAmero", "MATH_NUMBER_TOOLTIP": "Um número.", - "MATH_TRIG_SIN": "sin", + "MATH_TRIG_SIN": "sen", "MATH_TRIG_COS": "cos", "MATH_TRIG_TAN": "tan", "MATH_TRIG_ASIN": "asin", @@ -342,6 +343,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "faça algo", "PROCEDURES_BEFORE_PARAMS": "com:", "PROCEDURES_CALL_BEFORE_PARAMS": "com:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Não foi possível executar a função %1 definida pelo usuário porque o bloco de definição está desabilitado.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Cria uma função que não tem retorno.", "PROCEDURES_DEFNORETURN_COMMENT": "Descreva esta função...", "PROCEDURES_DEFRETURN_HELPURL": "https://pt.wikipedia.org/wiki/M%C3%A9todo_(programa%C3%A7%C3%A3o)", diff --git a/msg/json/pt.json b/packages/blockly/msg/json/pt.json similarity index 99% rename from msg/json/pt.json rename to packages/blockly/msg/json/pt.json index a024b1b30f1..f51544acc9b 100644 --- a/msg/json/pt.json +++ b/packages/blockly/msg/json/pt.json @@ -2,6 +2,7 @@ "@metadata": { "authors": [ "Athena in Wonderland", + "B3rnas", "Diniscoelho", "Fúlvio", "Hamilton Abreu", @@ -332,6 +333,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "faz algo", "PROCEDURES_BEFORE_PARAMS": "com:", "PROCEDURES_CALL_BEFORE_PARAMS": "com:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Não é possível executar a função definida pelo usuário '%1' porque o bloco de definição está desabilitado.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Cria uma função que não tem retorno.", "PROCEDURES_DEFNORETURN_COMMENT": "Descreva esta função...", "PROCEDURES_DEFRETURN_RETURN": "retorna", diff --git a/msg/json/qqq.json b/packages/blockly/msg/json/qqq.json similarity index 61% rename from msg/json/qqq.json rename to packages/blockly/msg/json/qqq.json index fcd8897bd04..6912c7fd5f7 100644 --- a/msg/json/qqq.json +++ b/packages/blockly/msg/json/qqq.json @@ -5,6 +5,7 @@ "Amire80", "Espertus", "Liuxinyu970226", + "McDutchie", "Metalhead64", "Nike", "Robby", @@ -18,12 +19,13 @@ "ADD_COMMENT": "context menu - Add a descriptive comment to the selected block.", "REMOVE_COMMENT": "context menu - Remove the descriptive comment from the selected block.", "DUPLICATE_COMMENT": "context menu - Make a copy of the selected workspace comment.\n{{Identical|Duplicate}}", - "EXTERNAL_INPUTS": "context menu - Change from 'external' to 'inline' mode for displaying blocks used as inputs to the selected block. See [[Translating:Blockly#context_menus]].", - "INLINE_INPUTS": "context menu - Change from 'internal' to 'external' mode for displaying blocks used as inputs to the selected block. See [[Translating:Blockly#context_menus]].", + "EXTERNAL_INPUTS": "context menu - Change from 'external' to 'inline' mode for displaying blocks used as inputs to the selected block. See [[Translating:Blockly#context_menus]].\n\nThe opposite of {{msg-blockly|INLINE INPUTS}}.", + "INLINE_INPUTS": "context menu - Change from 'internal' to 'external' mode for displaying blocks used as inputs to the selected block. See [[Translating:Blockly#context_menus]].\n\nThe opposite of {{msg-blockly|EXTERNAL INPUTS}}.", "DELETE_BLOCK": "context menu - Permanently delete the selected block.", "DELETE_X_BLOCKS": "context menu - Permanently delete the %1 selected blocks.\n\nParameters:\n* %1 - an integer greater than 1.", "DELETE_ALL_BLOCKS": "confirmation prompt - Question the user if they really wanted to permanently delete all %1 blocks.\n\nParameters:\n* %1 - an integer greater than 1.", "CLEAN_UP": "context menu - Reposition all the blocks so that they form a neat line.", + "CLOSE": "toast notification - Accessibility label for close button.", "COLLAPSE_BLOCK": "context menu - Make the appearance of the selected block smaller by hiding some information about it.", "COLLAPSE_ALL": "context menu - Make the appearance of all blocks smaller by hiding some information about it. Use the same terminology as in the previous message.", "EXPAND_BLOCK": "context menu - Restore the appearance of the selected block by showing information about it that was hidden (collapsed) earlier.", @@ -34,14 +36,14 @@ "UNDO": "context menu - Undo the previous action.\n{{Identical|Undo}}", "REDO": "context menu - Undo the previous undo action.\n{{Identical|Redo}}", "CHANGE_VALUE_TITLE": "prompt - This message is seen on mobile devices and the Opera browser. With most browsers, users can edit numeric values in blocks by just clicking and typing. Opera does not allow this and mobile browsers may have issues with in-line textareas. So we prompt users with this message (usually a popup) to change a value.", - "RENAME_VARIABLE": "dropdown choice - When the user clicks on a variable block, this is one of the dropdown menu choices. It is used to rename the current variable. See [https://github.com/google/blockly/wiki/Variables#dropdown-menu https://github.com/google/blockly/wiki/Variables#dropdown-menu].", - "RENAME_VARIABLE_TITLE": "prompt - Prompts the user to enter the new name for the selected variable. See [https://github.com/google/blockly/wiki/Variables#dropdown-menu https://github.com/google/blockly/wiki/Variables#dropdown-menu].\n\nParameters:\n* %1 - the name of the variable to be renamed.", + "RENAME_VARIABLE": "dropdown choice - When the user clicks on a variable block, this is one of the dropdown menu choices. It is used to rename the current variable. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Variables#dropdown-menu https://github.com/RaspberryPiFoundation/blockly/wiki/Variables#dropdown-menu].", + "RENAME_VARIABLE_TITLE": "prompt - Prompts the user to enter the new name for the selected variable. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Variables#dropdown-menu https://github.com/RaspberryPiFoundation/blockly/wiki/Variables#dropdown-menu].\n\nParameters:\n* %1 - the name of the variable to be renamed.", "NEW_VARIABLE": "button text - Text on the button used to launch the variable creation dialogue.", "NEW_STRING_VARIABLE": "button text - Text on the button used to launch the variable creation dialogue.", "NEW_NUMBER_VARIABLE": "button text - Text on the button used to launch the variable creation dialogue.", "NEW_COLOUR_VARIABLE": "button text - Text on the button used to launch the variable creation dialogue.", "NEW_VARIABLE_TYPE_TITLE": "prompt - Prompts the user to enter the type for a variable.", - "NEW_VARIABLE_TITLE": "prompt - Prompts the user to enter the name for a new variable. See [https://github.com/google/blockly/wiki/Variables#dropdown-menu https://github.com/google/blockly/wiki/Variables#dropdown-menu].", + "NEW_VARIABLE_TITLE": "prompt - Prompts the user to enter the name for a new variable. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Variables#dropdown-menu https://github.com/RaspberryPiFoundation/blockly/wiki/Variables#dropdown-menu].", "VARIABLE_ALREADY_EXISTS": "alert - Tells the user that the name they entered is already in use.", "VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE": "alert - Tells the user that the name they entered is already in use for another type.", "VARIABLE_ALREADY_EXISTS_FOR_A_PARAMETER": "alert - Tells the user that the name they entered is already in use as a parameter to a procedure, that the variable they are renaming also exists on. Renaming would create two parameters with the same name, which is not allowed.", @@ -49,54 +51,54 @@ "CANNOT_DELETE_VARIABLE_PROCEDURE": "alert - Tell the user that they can't delete a variable because it's part of the definition of a function.", "DELETE_VARIABLE": "dropdown choice - Delete the currently selected variable.", "COLOUR_PICKER_HELPURL": "{{Optional}} url - Information about colour.", - "COLOUR_PICKER_TOOLTIP": "tooltip - See [https://github.com/google/blockly/wiki/Colour#picking-a-colour-from-a-palette https://github.com/google/blockly/wiki/Colour#picking-a-colour-from-a-palette].", + "COLOUR_PICKER_TOOLTIP": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#picking-a-colour-from-a-palette https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#picking-a-colour-from-a-palette].", "COLOUR_RANDOM_HELPURL": "{{Optional}} url - A link that displays a random colour each time you visit it.", "COLOUR_RANDOM_TITLE": "block text - Title of block that generates a colour at random.", - "COLOUR_RANDOM_TOOLTIP": "tooltip - See [https://github.com/google/blockly/wiki/Colour#generating-a-random-colour https://github.com/google/blockly/wiki/Colour#generating-a-random-colour].", + "COLOUR_RANDOM_TOOLTIP": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#generating-a-random-colour https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#generating-a-random-colour].", "COLOUR_RGB_HELPURL": "{{Optional}} url - A link for colour codes with percentages (0-100%) for each component, instead of the more common 0-255, which may be more difficult for beginners.", - "COLOUR_RGB_TITLE": "block text - Title of block for [https://github.com/google/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components https://github.com/google/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components].", - "COLOUR_RGB_RED": "block input text - The amount of red (from 0 to 100) to use when [https://github.com/google/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components https://github.com/google/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components].\n{{Identical|Red}}", - "COLOUR_RGB_GREEN": "block input text - The amount of green (from 0 to 100) to use when [https://github.com/google/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components https://github.com/google/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components].", - "COLOUR_RGB_BLUE": "block input text - The amount of blue (from 0 to 100) to use when [https://github.com/google/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components https://github.com/google/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components].\n{{Identical|Blue}}", - "COLOUR_RGB_TOOLTIP": "tooltip - See [https://github.com/google/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components https://github.com/google/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components].", + "COLOUR_RGB_TITLE": "block text - Title of block for [https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components].", + "COLOUR_RGB_RED": "block input text - The amount of red (from 0 to 100) to use when [https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components].\n{{Identical|Red}}", + "COLOUR_RGB_GREEN": "block input text - The amount of green (from 0 to 100) to use when [https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components].", + "COLOUR_RGB_BLUE": "block input text - The amount of blue (from 0 to 100) to use when [https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components].\n{{Identical|Blue}}", + "COLOUR_RGB_TOOLTIP": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components].", "COLOUR_BLEND_HELPURL": "{{Optional}} url - A useful link that displays blending of two colours.", "COLOUR_BLEND_TITLE": "block text - A verb for blending two shades of paint.", - "COLOUR_BLEND_COLOUR1": "block input text - The first of two colours to [https://github.com/google/blockly/wiki/Colour#blending-colours blend].", - "COLOUR_BLEND_COLOUR2": "block input text - The second of two colours to [https://github.com/google/blockly/wiki/Colour#blending-colours blend].", - "COLOUR_BLEND_RATIO": "block input text - The proportion of the [https://github.com/google/blockly/wiki/Colour#blending-colours blend] containing the first colour; the remaining proportion is of the second colour. For example, if the first colour is red and the second colour blue, a ratio of 1 would yield pure red, a ratio of .5 would yield purple (equal amounts of red and blue), and a ratio of 0 would yield pure blue.\n{{Identical|Ratio}}", - "COLOUR_BLEND_TOOLTIP": "tooltip - See [https://github.com/google/blockly/wiki/Colour#blending-colours https://github.com/google/blockly/wiki/Colour#blending-colours].", + "COLOUR_BLEND_COLOUR1": "block input text - The first of two colours to [https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#blending-colours blend].", + "COLOUR_BLEND_COLOUR2": "block input text - The second of two colours to [https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#blending-colours blend].", + "COLOUR_BLEND_RATIO": "block input text - The proportion of the [https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#blending-colours blend] containing the first colour; the remaining proportion is of the second colour. For example, if the first colour is red and the second colour blue, a ratio of 1 would yield pure red, a ratio of .5 would yield purple (equal amounts of red and blue), and a ratio of 0 would yield pure blue.\n{{Identical|Ratio}}", + "COLOUR_BLEND_TOOLTIP": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#blending-colours https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#blending-colours].", "CONTROLS_REPEAT_HELPURL": "{{Optional}} url - Describes 'repeat loops' in computer programs; consider using the translation of the page [https://en.wikipedia.org/wiki/Control_flow https://en.wikipedia.org/wiki/Control_flow].", - "CONTROLS_REPEAT_TITLE": "block input text - Title of [https://github.com/google/blockly/wiki/Loops#repeat repeat block].\n\nParameters:\n* %1 - the number of times the body of the loop should be repeated.", - "CONTROLS_REPEAT_INPUT_DO": "block text - Preceding the blocks in the body of the loop. See [https://github.com/google/blockly/wiki/Loops https://github.com/google/blockly/wiki/Loops].\n{{Identical|Do}}", - "CONTROLS_REPEAT_TOOLTIP": "tooltip - See [https://github.com/google/blockly/wiki/Loops#repeat https://github.com/google/blockly/wiki/Loops#repeat].", + "CONTROLS_REPEAT_TITLE": "block input text - Title of [https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#repeat repeat block].\n\nParameters:\n* %1 - the number of times the body of the loop should be repeated.", + "CONTROLS_REPEAT_INPUT_DO": "block text - Preceding the blocks in the body of the loop. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Loops https://github.com/RaspberryPiFoundation/blockly/wiki/Loops].\n{{Identical|Do}}", + "CONTROLS_REPEAT_TOOLTIP": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#repeat https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#repeat].", "CONTROLS_WHILEUNTIL_HELPURL": "{{Optional}} url - Describes 'while loops' in computer programs; consider using the translation of [https://en.wikipedia.org/wiki/While_loop https://en.wikipedia.org/wiki/While_loop], if present, or [https://en.wikipedia.org/wiki/Control_flow https://en.wikipedia.org/wiki/Control_flow].", - "CONTROLS_WHILEUNTIL_OPERATOR_WHILE": "dropdown - Specifies that a loop should [https://github.com/google/blockly/wiki/Loops#repeat-while repeat while] the following condition is true.", - "CONTROLS_WHILEUNTIL_OPERATOR_UNTIL": "dropdown - Specifies that a loop should [https://github.com/google/blockly/wiki/Loops#repeat-until repeat until] the following condition becomes true.", - "CONTROLS_WHILEUNTIL_TOOLTIP_WHILE": "tooltip - See [https://github.com/google/blockly/wiki/Loops#repeat-while Loops#repeat-while https://github.com/google/blockly/wiki/Loops#repeat-while Loops#repeat-while].", - "CONTROLS_WHILEUNTIL_TOOLTIP_UNTIL": "tooltip - See [https://github.com/google/blockly/wiki/Loops#repeat-until https://github.com/google/blockly/wiki/Loops#repeat-until].", + "CONTROLS_WHILEUNTIL_OPERATOR_WHILE": "dropdown - Specifies that a loop should [https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#repeat-while repeat while] the following condition is true.", + "CONTROLS_WHILEUNTIL_OPERATOR_UNTIL": "dropdown - Specifies that a loop should [https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#repeat-until repeat until] the following condition becomes true.", + "CONTROLS_WHILEUNTIL_TOOLTIP_WHILE": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#repeat-while Loops#repeat-while https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#repeat-while Loops#repeat-while].", + "CONTROLS_WHILEUNTIL_TOOLTIP_UNTIL": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#repeat-until https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#repeat-until].", "CONTROLS_FOR_HELPURL": "{{Optional}} url - Describes 'for loops' in computer programs. Consider using your language's translation of [https://en.wikipedia.org/wiki/For_loop https://en.wikipedia.org/wiki/For_loop], if present.", - "CONTROLS_FOR_TOOLTIP": "tooltip - See [https://github.com/google/blockly/wiki/Loops#count-with https://github.com/google/blockly/wiki/Loops#count-with].\n\nParameters:\n* %1 - the name of the loop variable.", - "CONTROLS_FOR_TITLE": "block text - Repeatedly counts a variable (%1) starting with a (usually lower) number in a range (%2), ending with a (usually higher) number in a range (%3), and counting the iterations by a number of steps (%4). As in [https://github.com/google/blockly/wiki/Loops#count-with https://github.com/google/blockly/wiki/Loops#count-with]. [[File:Blockly-count-with.png]]", + "CONTROLS_FOR_TOOLTIP": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#count-with https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#count-with].\n\nParameters:\n* %1 - the name of the loop variable.", + "CONTROLS_FOR_TITLE": "block text - Repeatedly counts a variable (%1) starting with a (usually lower) number in a range (%2), ending with a (usually higher) number in a range (%3), and counting the iterations by a number of steps (%4). As in [https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#count-with https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#count-with]. [[File:Blockly-count-with.png]]", "CONTROLS_FOREACH_HELPURL": "{{Optional}} url - Describes 'for-each loops' in computer programs. Consider using your language's translation of [https://en.wikipedia.org/wiki/Foreach https://en.wikipedia.org/wiki/Foreach] if present.", - "CONTROLS_FOREACH_TITLE": "block text - Title of [https://github.com/google/blockly/wiki/Loops#for-each for each block]. Sequentially assigns every item in array %2 to the valiable %1.", - "CONTROLS_FOREACH_TOOLTIP": "block text - Description of [https://github.com/google/blockly/wiki/Loops#for-each for each blocks].\n\nParameters:\n* %1 - the name of the loop variable.", + "CONTROLS_FOREACH_TITLE": "block text - Title of [https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#for-each for each block]. Sequentially assigns every item in array %2 to the valiable %1.", + "CONTROLS_FOREACH_TOOLTIP": "block text - Description of [https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#for-each for each blocks].\n\nParameters:\n* %1 - the name of the loop variable.", "CONTROLS_FLOW_STATEMENTS_HELPURL": "{{Optional}} url - Describes control flow in computer programs. Consider using your language's translation of [https://en.wikipedia.org/wiki/Control_flow https://en.wikipedia.org/wiki/Control_flow], if it exists.", - "CONTROLS_FLOW_STATEMENTS_OPERATOR_BREAK": "dropdown - The current loop should be exited. See [https://github.com/google/blockly/wiki/Loops#break https://github.com/google/blockly/wiki/Loops#break].", - "CONTROLS_FLOW_STATEMENTS_OPERATOR_CONTINUE": "dropdown - The current iteration of the loop should be ended and the next should begin. See [https://github.com/google/blockly/wiki/Loops#continue-with-next-iteration https://github.com/google/blockly/wiki/Loops#continue-with-next-iteration].", - "CONTROLS_FLOW_STATEMENTS_TOOLTIP_BREAK": "tooltip - See [https://github.com/google/blockly/wiki/Loops#break-out-of-loop https://github.com/google/blockly/wiki/Loops#break-out-of-loop].", - "CONTROLS_FLOW_STATEMENTS_TOOLTIP_CONTINUE": "tooltip - See [https://github.com/google/blockly/wiki/Loops#continue-with-next-iteration https://github.com/google/blockly/wiki/Loops#continue-with-next-iteration].", - "CONTROLS_FLOW_STATEMENTS_WARNING": "warning - The user has tried placing a block outside of a loop (for each, while, repeat, etc.), but this type of block may only be used within a loop. See [https://github.com/google/blockly/wiki/Loops#loop-termination-blocks https://github.com/google/blockly/wiki/Loops#loop-termination-blocks].", + "CONTROLS_FLOW_STATEMENTS_OPERATOR_BREAK": "dropdown - The current loop should be exited. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#break https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#break].", + "CONTROLS_FLOW_STATEMENTS_OPERATOR_CONTINUE": "dropdown - The current iteration of the loop should be ended and the next should begin. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#continue-with-next-iteration https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#continue-with-next-iteration].", + "CONTROLS_FLOW_STATEMENTS_TOOLTIP_BREAK": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#break-out-of-loop https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#break-out-of-loop].", + "CONTROLS_FLOW_STATEMENTS_TOOLTIP_CONTINUE": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#continue-with-next-iteration https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#continue-with-next-iteration].", + "CONTROLS_FLOW_STATEMENTS_WARNING": "warning - The user has tried placing a block outside of a loop (for each, while, repeat, etc.), but this type of block may only be used within a loop. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#loop-termination-blocks https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#loop-termination-blocks].", "CONTROLS_IF_HELPURL": "{{Optional}} url - Describes conditional statements (if-then-else) in computer programs. Consider using your language's translation of [https://en.wikipedia.org/wiki/If_else https://en.wikipedia.org/wiki/If_else], if present.", - "CONTROLS_IF_TOOLTIP_1": "tooltip - Describes [https://github.com/google/blockly/wiki/IfElse#if-blocks 'if' blocks]. Consider using your language's translation of [https://en.wikipedia.org/wiki/If_statement https://en.wikipedia.org/wiki/If_statement], if present.", - "CONTROLS_IF_TOOLTIP_2": "tooltip - Describes [https://github.com/google/blockly/wiki/IfElse#if-else-blocks if-else blocks]. Consider using your language's translation of [https://en.wikipedia.org/wiki/If_statement https://en.wikipedia.org/wiki/If_statement], if present.", - "CONTROLS_IF_TOOLTIP_3": "tooltip - Describes [https://github.com/google/blockly/wiki/IfElse#if-else-if-blocks if-else-if blocks]. Consider using your language's translation of [https://en.wikipedia.org/wiki/If_statement https://en.wikipedia.org/wiki/If_statement], if present.", - "CONTROLS_IF_TOOLTIP_4": "tooltip - Describes [https://github.com/google/blockly/wiki/IfElse#if-else-if-else-blocks if-else-if-else blocks]. Consider using your language's translation of [https://en.wikipedia.org/wiki/If_statement https://en.wikipedia.org/wiki/If_statement], if present.", - "CONTROLS_IF_MSG_IF": "block text - See [https://github.com/google/blockly/wiki/IfElse https://github.com/google/blockly/wiki/IfElse]. It is recommended, but not essential, that this have text in common with the translation of 'else if'\n{{Identical|If}}", - "CONTROLS_IF_MSG_ELSEIF": "block text - See [https://github.com/google/blockly/wiki/IfElse https://github.com/google/blockly/wiki/IfElse]. The English words 'otherwise if' would probably be clearer than 'else if', but the latter is used because it is traditional and shorter.", - "CONTROLS_IF_MSG_ELSE": "block text - See [https://github.com/google/blockly/wiki/IfElse https://github.com/google/blockly/wiki/IfElse]. The English word 'otherwise' would probably be superior to 'else', but the latter is used because it is traditional and shorter.", - "CONTROLS_IF_IF_TOOLTIP": "tooltip - Describes [https://github.com/google/blockly/wiki/IfElse#block-modification if block modification].", - "CONTROLS_IF_ELSEIF_TOOLTIP": "tooltip - Describes the 'else if' subblock during [https://github.com/google/blockly/wiki/IfElse#block-modification if block modification].", - "CONTROLS_IF_ELSE_TOOLTIP": "tooltip - Describes the 'else' subblock during [https://github.com/google/blockly/wiki/IfElse#block-modification if block modification].", + "CONTROLS_IF_TOOLTIP_1": "tooltip - Describes [https://github.com/RaspberryPiFoundation/blockly/wiki/IfElse#if-blocks 'if' blocks]. Consider using your language's translation of [https://en.wikipedia.org/wiki/If_statement https://en.wikipedia.org/wiki/If_statement], if present.", + "CONTROLS_IF_TOOLTIP_2": "tooltip - Describes [https://github.com/RaspberryPiFoundation/blockly/wiki/IfElse#if-else-blocks if-else blocks]. Consider using your language's translation of [https://en.wikipedia.org/wiki/If_statement https://en.wikipedia.org/wiki/If_statement], if present.", + "CONTROLS_IF_TOOLTIP_3": "tooltip - Describes [https://github.com/RaspberryPiFoundation/blockly/wiki/IfElse#if-else-if-blocks if-else-if blocks]. Consider using your language's translation of [https://en.wikipedia.org/wiki/If_statement https://en.wikipedia.org/wiki/If_statement], if present.", + "CONTROLS_IF_TOOLTIP_4": "tooltip - Describes [https://github.com/RaspberryPiFoundation/blockly/wiki/IfElse#if-else-if-else-blocks if-else-if-else blocks]. Consider using your language's translation of [https://en.wikipedia.org/wiki/If_statement https://en.wikipedia.org/wiki/If_statement], if present.", + "CONTROLS_IF_MSG_IF": "block text - See [https://github.com/RaspberryPiFoundation/blockly/wiki/IfElse https://github.com/RaspberryPiFoundation/blockly/wiki/IfElse]. It is recommended, but not essential, that this have text in common with the translation of 'else if'\n{{Identical|If}}", + "CONTROLS_IF_MSG_ELSEIF": "block text - See [https://github.com/RaspberryPiFoundation/blockly/wiki/IfElse https://github.com/RaspberryPiFoundation/blockly/wiki/IfElse]. The English words 'otherwise if' would probably be clearer than 'else if', but the latter is used because it is traditional and shorter.", + "CONTROLS_IF_MSG_ELSE": "block text - See [https://github.com/RaspberryPiFoundation/blockly/wiki/IfElse https://github.com/RaspberryPiFoundation/blockly/wiki/IfElse]. The English word 'otherwise' would probably be superior to 'else', but the latter is used because it is traditional and shorter.", + "CONTROLS_IF_IF_TOOLTIP": "tooltip - Describes [https://github.com/RaspberryPiFoundation/blockly/wiki/IfElse#block-modification if block modification].", + "CONTROLS_IF_ELSEIF_TOOLTIP": "tooltip - Describes the 'else if' subblock during [https://github.com/RaspberryPiFoundation/blockly/wiki/IfElse#block-modification if block modification].", + "CONTROLS_IF_ELSE_TOOLTIP": "tooltip - Describes the 'else' subblock during [https://github.com/RaspberryPiFoundation/blockly/wiki/IfElse#block-modification if block modification].", "LOGIC_COMPARE_HELPURL": "{{Optional}} url - Information about comparisons.", "LOGIC_COMPARE_TOOLTIP_EQ": "tooltip - Describes the equals (=) block.", "LOGIC_COMPARE_TOOLTIP_NEQ": "tooltip - Describes the not equals (≠) block.", @@ -211,64 +213,64 @@ "MATH_ATAN2_TITLE": "block text - The title of the block that calculates atan2 of point (X, Y). For example, if the point is (-1, -1), this returns -135. %1 is a placeholder for the X coordinate, %2 is the placeholder for the Y coordinate.", "MATH_ATAN2_TOOLTIP": "tooltip - Return the arctangent of point (X, Y) in degrees from -180 to 180. For example, if the point is (-1, -1) this returns -135.", "TEXT_TEXT_HELPURL": "{{Optional}} url - Information about how computers represent text (sometimes referred to as ''string''s).", - "TEXT_TEXT_TOOLTIP": "tooltip - See [https://github.com/google/blockly/wiki/Text https://github.com/google/blockly/wiki/Text].", + "TEXT_TEXT_TOOLTIP": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text https://github.com/RaspberryPiFoundation/blockly/wiki/Text].", "TEXT_JOIN_HELPURL": "{{Optional}} url - Information on concatenating/appending pieces of text.", - "TEXT_JOIN_TITLE_CREATEWITH": "block text - See [https://github.com/google/blockly/wiki/Text#text-creation https://github.com/google/blockly/wiki/Text#text-creation].", - "TEXT_JOIN_TOOLTIP": "tooltip - See [https://github.com/google/blockly/wiki/Text#text-creation create text with] for more information.", - "TEXT_CREATE_JOIN_TITLE_JOIN": "block text - This is shown when the programmer wants to change the number of pieces of text being joined together. See [https://github.com/google/blockly/wiki/Text#text-creation https://github.com/google/blockly/wiki/Text#text-creation], specifically the last picture in the 'Text creation' section.\n{{Identical|Join}}", - "TEXT_CREATE_JOIN_TOOLTIP": "tooltip - See [https://github.com/google/blockly/wiki/Text#text-creation https://github.com/google/blockly/wiki/Text#text-creation], specifically the last picture in the 'Text creation' section.", - "TEXT_CREATE_JOIN_ITEM_TOOLTIP": "block text - See [https://github.com/google/blockly/wiki/Text#text-creation https://github.com/google/blockly/wiki/Text#text-creation], specifically the last picture in the 'Text creation' section.", + "TEXT_JOIN_TITLE_CREATEWITH": "block text - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#text-creation https://github.com/RaspberryPiFoundation/blockly/wiki/Text#text-creation].", + "TEXT_JOIN_TOOLTIP": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#text-creation create text with] for more information.", + "TEXT_CREATE_JOIN_TITLE_JOIN": "block text - This is shown when the programmer wants to change the number of pieces of text being joined together. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#text-creation https://github.com/RaspberryPiFoundation/blockly/wiki/Text#text-creation], specifically the last picture in the 'Text creation' section.\n{{Identical|Join}}", + "TEXT_CREATE_JOIN_TOOLTIP": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#text-creation https://github.com/RaspberryPiFoundation/blockly/wiki/Text#text-creation], specifically the last picture in the 'Text creation' section.", + "TEXT_CREATE_JOIN_ITEM_TOOLTIP": "block text - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#text-creation https://github.com/RaspberryPiFoundation/blockly/wiki/Text#text-creation], specifically the last picture in the 'Text creation' section.", "TEXT_APPEND_HELPURL": "{{Optional}} url - This and the other text-related URLs are going to be hard to translate. As always, it is okay to leave untranslated or paste in the English-language URL. For these URLs, you might also consider a general URL about how computers represent text (such as the translation of [https://en.wikipedia.org/wiki/String_(computer_science) this Wikipedia page]).", "TEXT_APPEND_TITLE": "block input text - Message that the variable name at %1 will have the item at %2 appended to it. [[File:blockly-append-text.png]]", - "TEXT_APPEND_TOOLTIP": "tooltip - See [https://github.com/google/blockly/wiki/Text#text-modification https://github.com/google/blockly/wiki/Text#text-modification] for more information.\n\nParameters:\n* %1 - the name of the variable to which text should be appended", + "TEXT_APPEND_TOOLTIP": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#text-modification https://github.com/RaspberryPiFoundation/blockly/wiki/Text#text-modification] for more information.\n\nParameters:\n* %1 - the name of the variable to which text should be appended", "TEXT_LENGTH_HELPURL": "{{Optional}} url - Information about text on computers (usually referred to as 'strings').", - "TEXT_LENGTH_TITLE": "block text - See [https://github.com/google/blockly/wiki/Text#text-length https://github.com/google/blockly/wiki/Text#text-length]. \n\nParameters:\n* %1 - the piece of text to take the length of", - "TEXT_LENGTH_TOOLTIP": "tooltip - See [https://github.com/google/blockly/wiki/Text#text-length https://github.com/google/blockly/wiki/Text#text-length].", + "TEXT_LENGTH_TITLE": "block text - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#text-length https://github.com/RaspberryPiFoundation/blockly/wiki/Text#text-length]. \n\nParameters:\n* %1 - the piece of text to take the length of", + "TEXT_LENGTH_TOOLTIP": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#text-length https://github.com/RaspberryPiFoundation/blockly/wiki/Text#text-length].", "TEXT_ISEMPTY_HELPURL": "{{Optional}} url - Information about empty pieces of text on computers (usually referred to as 'empty strings').", - "TEXT_ISEMPTY_TITLE": "block text - See [https://github.com/google/blockly/wiki/Text#checking-for-empty-text https://github.com/google/blockly/wiki/Text#checking-for-empty-text]. \n\nParameters:\n* %1 - the piece of text to test for emptiness", - "TEXT_ISEMPTY_TOOLTIP": "tooltip - See [https://github.com/google/blockly/wiki/Text#checking-for-empty-text https://github.com/google/blockly/wiki/Text#checking-for-empty-text].", + "TEXT_ISEMPTY_TITLE": "block text - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#checking-for-empty-text https://github.com/RaspberryPiFoundation/blockly/wiki/Text#checking-for-empty-text]. \n\nParameters:\n* %1 - the piece of text to test for emptiness", + "TEXT_ISEMPTY_TOOLTIP": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#checking-for-empty-text https://github.com/RaspberryPiFoundation/blockly/wiki/Text#checking-for-empty-text].", "TEXT_INDEXOF_HELPURL": "{{Optional}} url - Information about finding a character in a piece of text.", - "TEXT_INDEXOF_TOOLTIP": "tooltip - %1 will be replaced by either the number 0 or -1 depending on the indexing mode. See [https://github.com/google/blockly/wiki/Text#finding-text https://github.com/google/blockly/wiki/Text#finding-text].", - "TEXT_INDEXOF_TITLE": "block text - Title of blocks allowing users to find text. See [https://github.com/google/blockly/wiki/Text#finding-text https://github.com/google/blockly/wiki/Text#finding-text]. [[File:Blockly-find-text.png]]. In English the expanded message is 'in text %1 find (first|last) occurance of text %3' where %1 and %3 are added by the user. See TEXT_INDEXOF_OPERATOR_FIRST and TEXT_INDEXOF_OPERATOR_LAST for the dropdown text that replaces %2.", - "TEXT_INDEXOF_OPERATOR_FIRST": "dropdown - See [https://github.com/google/blockly/wiki/Text#finding-text https://github.com/google/blockly/wiki/Text#finding-text]. [[File:Blockly-find-text.png]].", - "TEXT_INDEXOF_OPERATOR_LAST": "dropdown - See [https://github.com/google/blockly/wiki/Text#finding-text https://github.com/google/blockly/wiki/Text#finding-text]. This would replace 'find first occurrence of text' below. (For more information on how common text is factored out of dropdown menus, see [https://translatewiki.net/wiki/Translating:Blockly#Drop-Down_Menus https://translatewiki.net/wiki/Translating:Blockly#Drop-Down_Menus)].) [[File:Blockly-find-text.png]].", + "TEXT_INDEXOF_TOOLTIP": "tooltip - %1 will be replaced by either the number 0 or -1 depending on the indexing mode. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#finding-text https://github.com/RaspberryPiFoundation/blockly/wiki/Text#finding-text].", + "TEXT_INDEXOF_TITLE": "block text - Title of blocks allowing users to find text. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#finding-text https://github.com/RaspberryPiFoundation/blockly/wiki/Text#finding-text]. [[File:Blockly-find-text.png]]. In English the expanded message is 'in text %1 find (first|last) occurance of text %3' where %1 and %3 are added by the user. See TEXT_INDEXOF_OPERATOR_FIRST and TEXT_INDEXOF_OPERATOR_LAST for the dropdown text that replaces %2.", + "TEXT_INDEXOF_OPERATOR_FIRST": "dropdown - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#finding-text https://github.com/RaspberryPiFoundation/blockly/wiki/Text#finding-text]. [[File:Blockly-find-text.png]].", + "TEXT_INDEXOF_OPERATOR_LAST": "dropdown - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#finding-text https://github.com/RaspberryPiFoundation/blockly/wiki/Text#finding-text]. This would replace 'find first occurrence of text' below. (For more information on how common text is factored out of dropdown menus, see [https://translatewiki.net/wiki/Translating:Blockly#Drop-Down_Menus https://translatewiki.net/wiki/Translating:Blockly#Drop-Down_Menus)].) [[File:Blockly-find-text.png]].", "TEXT_CHARAT_HELPURL": "{{Optional}} url - Information about extracting characters (letters, number, symbols, etc.) from text.", - "TEXT_CHARAT_TITLE": "block text - Text for a block to extract a letter (or number, punctuation character, etc.) from a string, as shown below. %1 is added by the user and %2 is replaced by a dropdown of options, possibly followed by another user supplied string. TEXT_CHARAT_TAIL is then added to the end. See [https://github.com/google/blockly/wiki/Text#extracting-a-single-character https://github.com/google/blockly/wiki/Text#extracting-a-single-character]. [[File:Blockly-text-get.png]]", - "TEXT_CHARAT_FROM_START": "dropdown - Indicates that the letter (or number, punctuation character, etc.) with the specified index should be obtained from the preceding piece of text. See [https://github.com/google/blockly/wiki/Text#extracting-a-single-character https://github.com/google/blockly/wiki/Text#extracting-a-single-character]. [[File:Blockly-text-get.png]]", - "TEXT_CHARAT_FROM_END": "block text - Indicates that the letter (or number, punctuation character, etc.) with the specified index from the end of a given piece of text should be obtained. See [https://github.com/google/blockly/wiki/Text#extracting-a-single-character https://github.com/google/blockly/wiki/Text#extracting-a-single-character]. [[File:Blockly-text-get.png]]", - "TEXT_CHARAT_FIRST": "block text - Indicates that the first letter of the following piece of text should be retrieved. See [https://github.com/google/blockly/wiki/Text#extracting-a-single-character https://github.com/google/blockly/wiki/Text#extracting-a-single-character]. [[File:Blockly-text-get.png]]", - "TEXT_CHARAT_LAST": "block text - Indicates that the last letter (or number, punctuation mark, etc.) of the following piece of text should be retrieved. See [https://github.com/google/blockly/wiki/Text#extracting-a-single-character https://github.com/google/blockly/wiki/Text#extracting-a-single-character]. [[File:Blockly-text-get.png]]", - "TEXT_CHARAT_RANDOM": "block text - Indicates that any letter (or number, punctuation mark, etc.) in the following piece of text should be randomly selected. See [https://github.com/google/blockly/wiki/Text#extracting-a-single-character https://github.com/google/blockly/wiki/Text#extracting-a-single-character]. [[File:Blockly-text-get.png]]", + "TEXT_CHARAT_TITLE": "block text - Text for a block to extract a letter (or number, punctuation character, etc.) from a string, as shown below. %1 is added by the user and %2 is replaced by a dropdown of options, possibly followed by another user supplied string. TEXT_CHARAT_TAIL is then added to the end. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-single-character https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-single-character]. [[File:Blockly-text-get.png]]", + "TEXT_CHARAT_FROM_START": "dropdown - Indicates that the letter (or number, punctuation character, etc.) with the specified index should be obtained from the preceding piece of text. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-single-character https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-single-character]. [[File:Blockly-text-get.png]]", + "TEXT_CHARAT_FROM_END": "block text - Indicates that the letter (or number, punctuation character, etc.) with the specified index from the end of a given piece of text should be obtained. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-single-character https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-single-character]. [[File:Blockly-text-get.png]]", + "TEXT_CHARAT_FIRST": "block text - Indicates that the first letter of the following piece of text should be retrieved. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-single-character https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-single-character]. [[File:Blockly-text-get.png]]", + "TEXT_CHARAT_LAST": "block text - Indicates that the last letter (or number, punctuation mark, etc.) of the following piece of text should be retrieved. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-single-character https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-single-character]. [[File:Blockly-text-get.png]]", + "TEXT_CHARAT_RANDOM": "block text - Indicates that any letter (or number, punctuation mark, etc.) in the following piece of text should be randomly selected. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-single-character https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-single-character]. [[File:Blockly-text-get.png]]", "TEXT_CHARAT_TAIL": "{{Optional|Supply translation only if your language requires it. Most do not.}} block text - Text that goes after the rightmost block/dropdown when getting a single letter from a piece of text, as in [https://blockly-demo.appspot.com/static/apps/code/index.html#3m23km these blocks] or shown below. For most languages, this will be blank. [[File:Blockly-text-get.png]]", - "TEXT_CHARAT_TOOLTIP": "tooltip - See [https://github.com/google/blockly/wiki/Text#extracting-a-single-character https://github.com/google/blockly/wiki/Text#extracting-a-single-character]. [[File:Blockly-text-get.png]]", - "TEXT_GET_SUBSTRING_TOOLTIP": "See [https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text].", + "TEXT_CHARAT_TOOLTIP": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-single-character https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-single-character]. [[File:Blockly-text-get.png]]", + "TEXT_GET_SUBSTRING_TOOLTIP": "See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-region-of-text https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-region-of-text].", "TEXT_GET_SUBSTRING_HELPURL": "{{Optional}} url - Information about extracting characters from text. Reminder: urls are the lowest priority translations. Feel free to skip.", "TEXT_GET_SUBSTRING_INPUT_IN_TEXT": "block text - Precedes a piece of text from which a portion should be extracted. [[File:Blockly-get-substring.png]]", - "TEXT_GET_SUBSTRING_START_FROM_START": "dropdown - Indicates that the following number specifies the position (relative to the start position) of the beginning of the region of text that should be obtained from the preceding piece of text. See [https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text]. [[File:Blockly-get-substring.png]]", - "TEXT_GET_SUBSTRING_START_FROM_END": "dropdown - Indicates that the following number specifies the position (relative to the end position) of the beginning of the region of text that should be obtained from the preceding piece of text. See [https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text]. Note: If {{msg-blockly|ORDINAL_NUMBER_SUFFIX}} is defined, it will automatically appear ''after'' this and any other [https://translatewiki.net/wiki/Translating:Blockly#Ordinal_numbers ordinal numbers] on this block. [[File:Blockly-get-substring.png]]", - "TEXT_GET_SUBSTRING_START_FIRST": "block text - Indicates that a region starting with the first letter of the preceding piece of text should be extracted. See [https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text]. [[File:Blockly-get-substring.png]]", - "TEXT_GET_SUBSTRING_END_FROM_START": "dropdown - Indicates that the following number specifies the position (relative to the start position) of the end of the region of text that should be obtained from the preceding piece of text. See [https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text]. [[File:Blockly-get-substring.png]]", - "TEXT_GET_SUBSTRING_END_FROM_END": "dropdown - Indicates that the following number specifies the position (relative to the end position) of the end of the region of text that should be obtained from the preceding piece of text. See [https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text]. [[File:Blockly-get-substring.png]]", - "TEXT_GET_SUBSTRING_END_LAST": "block text - Indicates that a region ending with the last letter of the preceding piece of text should be extracted. See [https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text]. [[File:Blockly-get-substring.png]]", - "TEXT_GET_SUBSTRING_TAIL": "{{Optional|Supply translation only if your language requires it. Most do not.}} block text - Text that should go after the rightmost block/dropdown when [https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text extracting a region of text]. In most languages, this will be the empty string. [[File:Blockly-get-substring.png]]", + "TEXT_GET_SUBSTRING_START_FROM_START": "dropdown - Indicates that the following number specifies the position (relative to the start position) of the beginning of the region of text that should be obtained from the preceding piece of text. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-region-of-text https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-region-of-text]. [[File:Blockly-get-substring.png]]", + "TEXT_GET_SUBSTRING_START_FROM_END": "dropdown - Indicates that the following number specifies the position (relative to the end position) of the beginning of the region of text that should be obtained from the preceding piece of text. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-region-of-text https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-region-of-text]. Note: If {{msg-blockly|ORDINAL_NUMBER_SUFFIX}} is defined, it will automatically appear ''after'' this and any other [https://translatewiki.net/wiki/Translating:Blockly#Ordinal_numbers ordinal numbers] on this block. [[File:Blockly-get-substring.png]]", + "TEXT_GET_SUBSTRING_START_FIRST": "block text - Indicates that a region starting with the first letter of the preceding piece of text should be extracted. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-region-of-text https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-region-of-text]. [[File:Blockly-get-substring.png]]", + "TEXT_GET_SUBSTRING_END_FROM_START": "dropdown - Indicates that the following number specifies the position (relative to the start position) of the end of the region of text that should be obtained from the preceding piece of text. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-region-of-text https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-region-of-text]. [[File:Blockly-get-substring.png]]", + "TEXT_GET_SUBSTRING_END_FROM_END": "dropdown - Indicates that the following number specifies the position (relative to the end position) of the end of the region of text that should be obtained from the preceding piece of text. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-region-of-text https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-region-of-text]. [[File:Blockly-get-substring.png]]", + "TEXT_GET_SUBSTRING_END_LAST": "block text - Indicates that a region ending with the last letter of the preceding piece of text should be extracted. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-region-of-text https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-region-of-text]. [[File:Blockly-get-substring.png]]", + "TEXT_GET_SUBSTRING_TAIL": "{{Optional|Supply translation only if your language requires it. Most do not.}} block text - Text that should go after the rightmost block/dropdown when [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-region-of-text extracting a region of text]. In most languages, this will be the empty string. [[File:Blockly-get-substring.png]]", "TEXT_CHANGECASE_HELPURL": "{{Optional}} url - Information about the case of letters (upper-case and lower-case).", - "TEXT_CHANGECASE_TOOLTIP": "tooltip - Describes a block to adjust the case of letters. For more information on this block, see [https://github.com/google/blockly/wiki/Text#adjusting-text-case https://github.com/google/blockly/wiki/Text#adjusting-text-case].", - "TEXT_CHANGECASE_OPERATOR_UPPERCASE": "block text - Indicates that all of the letters in the following piece of text should be capitalized. If your language does not use case, you may indicate that this is not applicable to your language. For more information on this block, see [https://github.com/google/blockly/wiki/Text#adjusting-text-case https://github.com/google/blockly/wiki/Text#adjusting-text-case].", - "TEXT_CHANGECASE_OPERATOR_LOWERCASE": "block text - Indicates that all of the letters in the following piece of text should be converted to lower-case. If your language does not use case, you may indicate that this is not applicable to your language. For more information on this block, see [https://github.com/google/blockly/wiki/Text#adjusting-text-case https://github.com/google/blockly/wiki/Text#adjusting-text-case].", - "TEXT_CHANGECASE_OPERATOR_TITLECASE": "block text - Indicates that the first letter of each of the following words should be capitalized and the rest converted to lower-case. If your language does not use case, you may indicate that this is not applicable to your language. For more information on this block, see [https://github.com/google/blockly/wiki/Text#adjusting-text-case https://github.com/google/blockly/wiki/Text#adjusting-text-case].", + "TEXT_CHANGECASE_TOOLTIP": "tooltip - Describes a block to adjust the case of letters. For more information on this block, see [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#adjusting-text-case https://github.com/RaspberryPiFoundation/blockly/wiki/Text#adjusting-text-case].", + "TEXT_CHANGECASE_OPERATOR_UPPERCASE": "block text - Indicates that all of the letters in the following piece of text should be capitalized. If your language does not use case, you may indicate that this is not applicable to your language. For more information on this block, see [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#adjusting-text-case https://github.com/RaspberryPiFoundation/blockly/wiki/Text#adjusting-text-case].", + "TEXT_CHANGECASE_OPERATOR_LOWERCASE": "block text - Indicates that all of the letters in the following piece of text should be converted to lower-case. If your language does not use case, you may indicate that this is not applicable to your language. For more information on this block, see [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#adjusting-text-case https://github.com/RaspberryPiFoundation/blockly/wiki/Text#adjusting-text-case].", + "TEXT_CHANGECASE_OPERATOR_TITLECASE": "block text - Indicates that the first letter of each of the following words should be capitalized and the rest converted to lower-case. If your language does not use case, you may indicate that this is not applicable to your language. For more information on this block, see [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#adjusting-text-case https://github.com/RaspberryPiFoundation/blockly/wiki/Text#adjusting-text-case].", "TEXT_TRIM_HELPURL": "{{Optional}} url - Information about trimming (removing) text off the beginning and ends of pieces of text.", - "TEXT_TRIM_TOOLTIP": "tooltip - See [https://github.com/google/blockly/wiki/Text#trimming-removing-spaces https://github.com/google/blockly/wiki/Text#trimming-removing-spaces].", - "TEXT_TRIM_OPERATOR_BOTH": "dropdown - Removes spaces from the beginning and end of a piece of text. See [https://github.com/google/blockly/wiki/Text#trimming-removing-spaces https://github.com/google/blockly/wiki/Text#trimming-removing-spaces]. Note that neither this nor the other options modify the original piece of text (that follows); the block just returns a version of the text without the specified spaces.", - "TEXT_TRIM_OPERATOR_LEFT": "dropdown - Removes spaces from the beginning of a piece of text. See [https://github.com/google/blockly/wiki/Text#trimming-removing-spaces https://github.com/google/blockly/wiki/Text#trimming-removing-spaces]. Note that in right-to-left scripts, this will remove spaces from the right side.", - "TEXT_TRIM_OPERATOR_RIGHT": "dropdown - Removes spaces from the end of a piece of text. See [https://github.com/google/blockly/wiki/Text#trimming-removing-spaces https://github.com/google/blockly/wiki/Text#trimming-removing-spaces]. Note that in right-to-left scripts, this will remove spaces from the left side.", + "TEXT_TRIM_TOOLTIP": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#trimming-removing-spaces https://github.com/RaspberryPiFoundation/blockly/wiki/Text#trimming-removing-spaces].", + "TEXT_TRIM_OPERATOR_BOTH": "dropdown - Removes spaces from the beginning and end of a piece of text. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#trimming-removing-spaces https://github.com/RaspberryPiFoundation/blockly/wiki/Text#trimming-removing-spaces]. Note that neither this nor the other options modify the original piece of text (that follows); the block just returns a version of the text without the specified spaces.", + "TEXT_TRIM_OPERATOR_LEFT": "dropdown - Removes spaces from the beginning of a piece of text. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#trimming-removing-spaces https://github.com/RaspberryPiFoundation/blockly/wiki/Text#trimming-removing-spaces]. Note that in right-to-left scripts, this will remove spaces from the right side.", + "TEXT_TRIM_OPERATOR_RIGHT": "dropdown - Removes spaces from the end of a piece of text. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#trimming-removing-spaces https://github.com/RaspberryPiFoundation/blockly/wiki/Text#trimming-removing-spaces]. Note that in right-to-left scripts, this will remove spaces from the left side.", "TEXT_PRINT_HELPURL": "{{Optional}} url - Information about displaying text on computers.", - "TEXT_PRINT_TITLE": "block text - Display the input on the screen. See [https://github.com/google/blockly/wiki/Text#printing-text https://github.com/google/blockly/wiki/Text#printing-text]. \n\nParameters:\n* %1 - the value to print", - "TEXT_PRINT_TOOLTIP": "tooltip - See [https://github.com/google/blockly/wiki/Text#printing-text https://github.com/google/blockly/wiki/Text#printing-text].", + "TEXT_PRINT_TITLE": "block text - Display the input on the screen. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#printing-text https://github.com/RaspberryPiFoundation/blockly/wiki/Text#printing-text]. \n\nParameters:\n* %1 - the value to print", + "TEXT_PRINT_TOOLTIP": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#printing-text https://github.com/RaspberryPiFoundation/blockly/wiki/Text#printing-text].", "TEXT_PROMPT_HELPURL": "{{Optional}} url - Information about getting text from users.", - "TEXT_PROMPT_TYPE_TEXT": "dropdown - Specifies that a piece of text should be requested from the user with the following message. See [https://github.com/google/blockly/wiki/Text#printing-text https://github.com/google/blockly/wiki/Text#printing-text].", - "TEXT_PROMPT_TYPE_NUMBER": "dropdown - Specifies that a number should be requested from the user with the following message. See [https://github.com/google/blockly/wiki/Text#printing-text https://github.com/google/blockly/wiki/Text#printing-text].", - "TEXT_PROMPT_TOOLTIP_NUMBER": "dropdown - Precedes the message with which the user should be prompted for a number. See [https://github.com/google/blockly/wiki/Text#printing-text https://github.com/google/blockly/wiki/Text#printing-text].", - "TEXT_PROMPT_TOOLTIP_TEXT": "dropdown - Precedes the message with which the user should be prompted for some text. See [https://github.com/google/blockly/wiki/Text#printing-text https://github.com/google/blockly/wiki/Text#printing-text].", + "TEXT_PROMPT_TYPE_TEXT": "dropdown - Specifies that a piece of text should be requested from the user with the following message. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#printing-text https://github.com/RaspberryPiFoundation/blockly/wiki/Text#printing-text].", + "TEXT_PROMPT_TYPE_NUMBER": "dropdown - Specifies that a number should be requested from the user with the following message. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#printing-text https://github.com/RaspberryPiFoundation/blockly/wiki/Text#printing-text].", + "TEXT_PROMPT_TOOLTIP_NUMBER": "dropdown - Precedes the message with which the user should be prompted for a number. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#printing-text https://github.com/RaspberryPiFoundation/blockly/wiki/Text#printing-text].", + "TEXT_PROMPT_TOOLTIP_TEXT": "dropdown - Precedes the message with which the user should be prompted for some text. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#printing-text https://github.com/RaspberryPiFoundation/blockly/wiki/Text#printing-text].", "TEXT_COUNT_MESSAGE0": "block text - Title of a block that counts the number of instances of a smaller pattern (%1) inside a longer string (%2).", "TEXT_COUNT_HELPURL": "{{Optional}} url - Information about counting how many times a string appears in another string.", "TEXT_COUNT_TOOLTIP": "tooltip - Short description of a block that counts how many times some text occurs within some other text.", @@ -277,78 +279,78 @@ "TEXT_REPLACE_TOOLTIP": "tooltip - Short description of a block that replaces copies of text in a large text with other text.", "TEXT_REVERSE_MESSAGE0": "block text - Title of block that returns a copy of text (%1) with the order of letters and characters reversed.", "TEXT_REVERSE_HELPURL": "{{Optional}} url - Information about reversing a letters/characters in text.", - "TEXT_REVERSE_TOOLTIP": "tooltip - See [https://github.com/google/blockly/wiki/Text].", + "TEXT_REVERSE_TOOLTIP": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text].", "LISTS_CREATE_EMPTY_HELPURL": "{{Optional}} url - Information on empty lists.", - "LISTS_CREATE_EMPTY_TITLE": "block text - See [https://github.com/google/blockly/wiki/Lists#create-empty-list https://github.com/google/blockly/wiki/Lists#create-empty-list].", - "LISTS_CREATE_EMPTY_TOOLTIP": "block text - See [https://github.com/google/blockly/wiki/Lists#create-empty-list https://github.com/google/blockly/wiki/Lists#create-empty-list].", + "LISTS_CREATE_EMPTY_TITLE": "block text - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#create-empty-list https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#create-empty-list].", + "LISTS_CREATE_EMPTY_TOOLTIP": "block text - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#create-empty-list https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#create-empty-list].", "LISTS_CREATE_WITH_HELPURL": "{{Optional}} url - Information on building lists.", - "LISTS_CREATE_WITH_TOOLTIP": "tooltip - See [https://github.com/google/blockly/wiki/Lists#create-list-with https://github.com/google/blockly/wiki/Lists#create-list-with].", - "LISTS_CREATE_WITH_INPUT_WITH": "block text - See [https://github.com/google/blockly/wiki/Lists#create-list-with https://github.com/google/blockly/wiki/Lists#create-list-with].", - "LISTS_CREATE_WITH_CONTAINER_TITLE_ADD": "block text - This appears in a sub-block when [https://github.com/google/blockly/wiki/Lists#changing-number-of-inputs changing the number of inputs in a ''''create list with'''' block].\n{{Identical|List}}", - "LISTS_CREATE_WITH_CONTAINER_TOOLTIP": "tooltip - See [https://github.com/google/blockly/wiki/Lists#changing-number-of-inputs https://github.com/google/blockly/wiki/Lists#changing-number-of-inputs].", - "LISTS_CREATE_WITH_ITEM_TOOLTIP": "tooltip - See [https://github.com/google/blockly/wiki/Lists#changing-number-of-inputs https://github.com/google/blockly/wiki/Lists#changing-number-of-inputs].", - "LISTS_REPEAT_HELPURL": "{{Optional}} url - Information about [https://github.com/google/blockly/wiki/Lists#create-list-with creating a list with multiple copies of a single item].", - "LISTS_REPEAT_TOOLTIP": "tooltip - See [https://github.com/google/blockly/wiki/Lists#create-list-with creating a list with multiple copies of a single item].", - "LISTS_REPEAT_TITLE": "block text - See [https://github.com/google/blockly/wiki/Lists#create-list-with https://github.com/google/blockly/wiki/Lists#create-list-with]. \n\nParameters:\n* %1 - the item (text) to be repeated\n* %2 - the number of times to repeat it", + "LISTS_CREATE_WITH_TOOLTIP": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#create-list-with https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#create-list-with].", + "LISTS_CREATE_WITH_INPUT_WITH": "block text - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#create-list-with https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#create-list-with].", + "LISTS_CREATE_WITH_CONTAINER_TITLE_ADD": "block text - This appears in a sub-block when [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#changing-number-of-inputs changing the number of inputs in a ''''create list with'''' block].\n{{Identical|List}}", + "LISTS_CREATE_WITH_CONTAINER_TOOLTIP": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#changing-number-of-inputs https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#changing-number-of-inputs].", + "LISTS_CREATE_WITH_ITEM_TOOLTIP": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#changing-number-of-inputs https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#changing-number-of-inputs].", + "LISTS_REPEAT_HELPURL": "{{Optional}} url - Information about [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#create-list-with creating a list with multiple copies of a single item].", + "LISTS_REPEAT_TOOLTIP": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#create-list-with creating a list with multiple copies of a single item].", + "LISTS_REPEAT_TITLE": "block text - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#create-list-with https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#create-list-with]. \n\nParameters:\n* %1 - the item (text) to be repeated\n* %2 - the number of times to repeat it", "LISTS_LENGTH_HELPURL": "{{Optional}} url - Information about how the length of a list is computed (i.e., by the total number of elements, not the number of different elements).", - "LISTS_LENGTH_TITLE": "block text - See [https://github.com/google/blockly/wiki/Lists#length-of https://github.com/google/blockly/wiki/Lists#length-of]. \n\nParameters:\n* %1 - the list whose length is desired", - "LISTS_LENGTH_TOOLTIP": "tooltip - See [https://github.com/google/blockly/wiki/Lists#length-of https://github.com/google/blockly/wiki/Lists#length-of Blockly:Lists:length of].", - "LISTS_ISEMPTY_HELPURL": "{{Optional}} url - See [https://github.com/google/blockly/wiki/Lists#is-empty https://github.com/google/blockly/wiki/Lists#is-empty].", - "LISTS_ISEMPTY_TITLE": "block text - See [https://github.com/google/blockly/wiki/Lists#is-empty https://github.com/google/blockly/wiki/Lists#is-empty]. \n\nParameters:\n* %1 - the list to test", - "LISTS_ISEMPTY_TOOLTIP": "block tooltip - See [https://github.com/google/blockly/wiki/Lists#is-empty https://github.com/google/blockly/wiki/Lists#is-empty].", - "LISTS_INLIST": "block text - Title of blocks operating on [https://github.com/google/blockly/wiki/Lists lists].", - "LISTS_INDEX_OF_HELPURL": "{{Optional}} url - See [https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list].", - "LISTS_INDEX_OF_FIRST": "dropdown - See [https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list Lists#finding-items-in-a-list]. [[File:Blockly-list-find.png]]", - "LISTS_INDEX_OF_LAST": "dropdown - See [https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list]. [[File:Blockly-list-find.png]]", - "LISTS_INDEX_OF_TOOLTIP": "tooltip - %1 will be replaced by either the number 0 or -1 depending on the indexing mode. See [https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list]. [[File:Blockly-list-find.png]]", - "LISTS_GET_INDEX_HELPURL": "{{Optional}} url - See [https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list].", - "LISTS_GET_INDEX_GET": "dropdown - Indicates that the user wishes to [https://github.com/google/blockly/wiki/Lists#getting-a-single-item get an item from a list] without removing it from the list.", - "LISTS_GET_INDEX_GET_REMOVE": "dropdown - Indicates that the user wishes to [https://github.com/google/blockly/wiki/Lists#getting-a-single-item get and remove an item from a list], as opposed to merely getting it without modifying the list.", - "LISTS_GET_INDEX_REMOVE": "dropdown - Indicates that the user wishes to [https://github.com/google/blockly/wiki/Lists#removing-an-item remove an item from a list].\n{{Identical|Remove}}", - "LISTS_GET_INDEX_FROM_START": "dropdown - Indicates that an index relative to the front of the list should be used to [https://github.com/google/blockly/wiki/Lists#getting-a-single-item get and/or remove an item from a list]. Note: If {{msg-blockly|ORDINAL_NUMBER_SUFFIX}} is defined, it will automatically appear ''after'' this number (and any other ordinal numbers on this block). See [[Translating:Blockly#Ordinal_numbers]] for more information on ordinal numbers in Blockly. [[File:Blockly-list-get-item.png]]", - "LISTS_GET_INDEX_FROM_END": "dropdown - Indicates that an index relative to the end of the list should be used to [https://github.com/google/blockly/wiki/Lists#getting-a-single-item access an item in a list]. [[File:Blockly-list-get-item.png]]", - "LISTS_GET_INDEX_FIRST": "dropdown - Indicates that the '''first''' item should be [https://github.com/google/blockly/wiki/Lists#getting-a-single-item accessed in a list]. [[File:Blockly-list-get-item.png]]", - "LISTS_GET_INDEX_LAST": "dropdown - Indicates that the '''last''' item should be [https://github.com/google/blockly/wiki/Lists#getting-a-single-item accessed in a list]. [[File:Blockly-list-get-item.png]]", - "LISTS_GET_INDEX_RANDOM": "dropdown - Indicates that a '''random''' item should be [https://github.com/google/blockly/wiki/Lists#getting-a-single-item accessed in a list]. [[File:Blockly-list-get-item.png]]", - "LISTS_GET_INDEX_TAIL": "{{Optional|Supply translation only if your language requires it. Most do not.}} block text - Text that should go after the rightmost block/dropdown when [https://github.com/google/blockly/wiki/Lists#getting-a-single-item accessing an item from a list]. In most languages, this will be the empty string. [[File:Blockly-list-get-item.png]]", + "LISTS_LENGTH_TITLE": "block text - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#length-of https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#length-of]. \n\nParameters:\n* %1 - the list whose length is desired", + "LISTS_LENGTH_TOOLTIP": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#length-of https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#length-of Blockly:Lists:length of].", + "LISTS_ISEMPTY_HELPURL": "{{Optional}} url - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#is-empty https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#is-empty].", + "LISTS_ISEMPTY_TITLE": "block text - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#is-empty https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#is-empty]. \n\nParameters:\n* %1 - the list to test", + "LISTS_ISEMPTY_TOOLTIP": "block tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#is-empty https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#is-empty].", + "LISTS_INLIST": "block text - Title of blocks operating on [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists lists].", + "LISTS_INDEX_OF_HELPURL": "{{Optional}} url - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#finding-items-in-a-list https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#finding-items-in-a-list].", + "LISTS_INDEX_OF_FIRST": "dropdown - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#finding-items-in-a-list Lists#finding-items-in-a-list]. [[File:Blockly-list-find.png]]", + "LISTS_INDEX_OF_LAST": "dropdown - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#finding-items-in-a-list https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#finding-items-in-a-list]. [[File:Blockly-list-find.png]]", + "LISTS_INDEX_OF_TOOLTIP": "tooltip - %1 will be replaced by either the number 0 or -1 depending on the indexing mode. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#finding-items-in-a-list https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#finding-items-in-a-list]. [[File:Blockly-list-find.png]]", + "LISTS_GET_INDEX_HELPURL": "{{Optional}} url - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-items-from-a-list https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-items-from-a-list].", + "LISTS_GET_INDEX_GET": "dropdown - Indicates that the user wishes to [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item get an item from a list] without removing it from the list.", + "LISTS_GET_INDEX_GET_REMOVE": "dropdown - Indicates that the user wishes to [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item get and remove an item from a list], as opposed to merely getting it without modifying the list.", + "LISTS_GET_INDEX_REMOVE": "dropdown - Indicates that the user wishes to [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#removing-an-item remove an item from a list].\n{{Identical|Remove}}", + "LISTS_GET_INDEX_FROM_START": "dropdown - Indicates that an index relative to the front of the list should be used to [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item get and/or remove an item from a list]. Note: If {{msg-blockly|ORDINAL_NUMBER_SUFFIX}} is defined, it will automatically appear ''after'' this number (and any other ordinal numbers on this block). See [[Translating:Blockly#Ordinal_numbers]] for more information on ordinal numbers in Blockly. [[File:Blockly-list-get-item.png]]", + "LISTS_GET_INDEX_FROM_END": "dropdown - Indicates that an index relative to the end of the list should be used to [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item access an item in a list]. [[File:Blockly-list-get-item.png]]", + "LISTS_GET_INDEX_FIRST": "dropdown - Indicates that the '''first''' item should be [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item accessed in a list]. [[File:Blockly-list-get-item.png]]", + "LISTS_GET_INDEX_LAST": "dropdown - Indicates that the '''last''' item should be [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item accessed in a list]. [[File:Blockly-list-get-item.png]]", + "LISTS_GET_INDEX_RANDOM": "dropdown - Indicates that a '''random''' item should be [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item accessed in a list]. [[File:Blockly-list-get-item.png]]", + "LISTS_GET_INDEX_TAIL": "{{Optional|Supply translation only if your language requires it. Most do not.}} block text - Text that should go after the rightmost block/dropdown when [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item accessing an item from a list]. In most languages, this will be the empty string. [[File:Blockly-list-get-item.png]]", "LISTS_INDEX_FROM_START_TOOLTIP": "tooltip - Indicates the ordinal number that the first item in a list is referenced by. %1 will be replaced by either '#0' or '#1' depending on the indexing mode.", "LISTS_INDEX_FROM_END_TOOLTIP": "tooltip - Indicates the ordinal number that the last item in a list is referenced by. %1 will be replaced by either '#0' or '#1' depending on the indexing mode.", - "LISTS_GET_INDEX_TOOLTIP_GET_FROM": "tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-a-single-item https://github.com/google/blockly/wiki/Lists#getting-a-single-item] for more information.", - "LISTS_GET_INDEX_TOOLTIP_GET_FIRST": "tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-a-single-item https://github.com/google/blockly/wiki/Lists#getting-a-single-item] for more information.", - "LISTS_GET_INDEX_TOOLTIP_GET_LAST": "tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-a-single-item https://github.com/google/blockly/wiki/Lists#getting-a-single-item] for more information.", - "LISTS_GET_INDEX_TOOLTIP_GET_RANDOM": "tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-a-single-item https://github.com/google/blockly/wiki/Lists#getting-a-single-item] for more information.", - "LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_FROM": "tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-and-removing-an-item] (for remove and return) and [https://github.com/google/blockly/wiki/Lists#getting-a-single-item] for '#' or '# from end'.", - "LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_FIRST": "tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-and-removing-an-item] (for remove and return) and [https://github.com/google/blockly/wiki/Lists#getting-a-single-item] for 'first'.", - "LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_LAST": "tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-and-removing-an-item] (for remove and return) and [https://github.com/google/blockly/wiki/Lists#getting-a-single-item] for 'last'.", - "LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_RANDOM": "tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-and-removing-an-item] (for remove and return) and [https://github.com/google/blockly/wiki/Lists#getting-a-single-item] for 'random'.", - "LISTS_GET_INDEX_TOOLTIP_REMOVE_FROM": "tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-and-removing-an-item] (for remove and return) and [https://github.com/google/blockly/wiki/Lists#getting-a-single-item] for '#' or '# from end'.", - "LISTS_GET_INDEX_TOOLTIP_REMOVE_FIRST": "tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-and-removing-an-item] (for remove and return) and [https://github.com/google/blockly/wiki/Lists#getting-a-single-item] for 'first'.", - "LISTS_GET_INDEX_TOOLTIP_REMOVE_LAST": "tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-and-removing-an-item] (for remove and return) and [https://github.com/google/blockly/wiki/Lists#getting-a-single-item] for 'last'.", - "LISTS_GET_INDEX_TOOLTIP_REMOVE_RANDOM": "tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-and-removing-an-item] (for remove and return) and [https://github.com/google/blockly/wiki/Lists#getting-a-single-item] for 'random'.", + "LISTS_GET_INDEX_TOOLTIP_GET_FROM": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item] for more information.", + "LISTS_GET_INDEX_TOOLTIP_GET_FIRST": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item] for more information.", + "LISTS_GET_INDEX_TOOLTIP_GET_LAST": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item] for more information.", + "LISTS_GET_INDEX_TOOLTIP_GET_RANDOM": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item] for more information.", + "LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_FROM": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-and-removing-an-item] (for remove and return) and [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item] for '#' or '# from end'.", + "LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_FIRST": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-and-removing-an-item] (for remove and return) and [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item] for 'first'.", + "LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_LAST": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-and-removing-an-item] (for remove and return) and [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item] for 'last'.", + "LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_RANDOM": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-and-removing-an-item] (for remove and return) and [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item] for 'random'.", + "LISTS_GET_INDEX_TOOLTIP_REMOVE_FROM": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-and-removing-an-item] (for remove and return) and [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item] for '#' or '# from end'.", + "LISTS_GET_INDEX_TOOLTIP_REMOVE_FIRST": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-and-removing-an-item] (for remove and return) and [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item] for 'first'.", + "LISTS_GET_INDEX_TOOLTIP_REMOVE_LAST": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-and-removing-an-item] (for remove and return) and [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item] for 'last'.", + "LISTS_GET_INDEX_TOOLTIP_REMOVE_RANDOM": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-and-removing-an-item] (for remove and return) and [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item] for 'random'.", "LISTS_SET_INDEX_HELPURL": "{{Optional}} url - Information about putting items in lists.", - "LISTS_SET_INDEX_SET": "block text - [https://github.com/google/blockly/wiki/Lists#in-list--set Replaces an item in a list]. [[File:Blockly-in-list-set-insert.png]]", - "LISTS_SET_INDEX_INSERT": "block text - [https://github.com/google/blockly/wiki/Lists#in-list--insert-at Inserts an item into a list]. [[File:Blockly-in-list-set-insert.png]]", + "LISTS_SET_INDEX_SET": "block text - [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#in-list--set Replaces an item in a list]. [[File:Blockly-in-list-set-insert.png]]", + "LISTS_SET_INDEX_INSERT": "block text - [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#in-list--insert-at Inserts an item into a list]. [[File:Blockly-in-list-set-insert.png]]", "LISTS_SET_INDEX_INPUT_TO": "block text - The word(s) after the position in the list and before the item to be set/inserted. [[File:Blockly-in-list-set-insert.png]]", - "LISTS_SET_INDEX_TOOLTIP_SET_FROM": "tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-a-single-item} (even though the page describes the 'get' block, the idea is the same for the 'set' block).", - "LISTS_SET_INDEX_TOOLTIP_SET_FIRST": "tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-a-single-item} (even though the page describes the 'get' block, the idea is the same for the 'set' block).", - "LISTS_SET_INDEX_TOOLTIP_SET_LAST": "tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-a-single-item} (even though the page describes the 'get' block, the idea is the same for the 'set' block).", - "LISTS_SET_INDEX_TOOLTIP_SET_RANDOM": "tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-a-single-item} (even though the page describes the 'get' block, the idea is the same for the 'set' block).", - "LISTS_SET_INDEX_TOOLTIP_INSERT_FROM": "tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-a-single-item} (even though the page describes the 'get' block, the idea is the same for the 'insert' block).", - "LISTS_SET_INDEX_TOOLTIP_INSERT_FIRST": "tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-a-single-item} (even though the page describes the 'get' block, the idea is the same for the 'insert' block).", - "LISTS_SET_INDEX_TOOLTIP_INSERT_LAST": "tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-a-single-item} (even though the page describes the 'get' block, the idea is the same for the 'insert' block).", - "LISTS_SET_INDEX_TOOLTIP_INSERT_RANDOM": "tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-a-single-item} (even though the page describes the 'get' block, the idea is the same for the 'insert' block).", + "LISTS_SET_INDEX_TOOLTIP_SET_FROM": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item} (even though the page describes the 'get' block, the idea is the same for the 'set' block).", + "LISTS_SET_INDEX_TOOLTIP_SET_FIRST": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item} (even though the page describes the 'get' block, the idea is the same for the 'set' block).", + "LISTS_SET_INDEX_TOOLTIP_SET_LAST": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item} (even though the page describes the 'get' block, the idea is the same for the 'set' block).", + "LISTS_SET_INDEX_TOOLTIP_SET_RANDOM": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item} (even though the page describes the 'get' block, the idea is the same for the 'set' block).", + "LISTS_SET_INDEX_TOOLTIP_INSERT_FROM": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item} (even though the page describes the 'get' block, the idea is the same for the 'insert' block).", + "LISTS_SET_INDEX_TOOLTIP_INSERT_FIRST": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item} (even though the page describes the 'get' block, the idea is the same for the 'insert' block).", + "LISTS_SET_INDEX_TOOLTIP_INSERT_LAST": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item} (even though the page describes the 'get' block, the idea is the same for the 'insert' block).", + "LISTS_SET_INDEX_TOOLTIP_INSERT_RANDOM": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item} (even though the page describes the 'get' block, the idea is the same for the 'insert' block).", "LISTS_GET_SUBLIST_HELPURL": "{{Optional}} url - Information describing extracting a sublist from an existing list.", - "LISTS_GET_SUBLIST_START_FROM_START": "dropdown - Indicates that an index relative to the front of the list should be used to specify the beginning of the range from which to [https://github.com/google/blockly/wiki/Lists#getting-a-sublist get a sublist]. [[File:Blockly-get-sublist.png]] Note: If {{msg-blockly|ORDINAL_NUMBER_SUFFIX}} is defined, it will automatically appear ''after'' this number (and any other ordinal numbers on this block). See [[Translating:Blockly#Ordinal_numbers]] for more information on ordinal numbers in Blockly.", - "LISTS_GET_SUBLIST_START_FROM_END": "dropdown - Indicates that an index relative to the end of the list should be used to specify the beginning of the range from which to [https://github.com/google/blockly/wiki/Lists#getting-a-sublist get a sublist].", - "LISTS_GET_SUBLIST_START_FIRST": "dropdown - Indicates that the [https://github.com/google/blockly/wiki/Lists#getting-a-sublist sublist to extract] should begin with the list's first item.", - "LISTS_GET_SUBLIST_END_FROM_START": "dropdown - Indicates that an index relative to the front of the list should be used to specify the end of the range from which to [https://github.com/google/blockly/wiki/Lists#getting-a-sublist get a sublist]. [[File:Blockly-get-sublist.png]]", - "LISTS_GET_SUBLIST_END_FROM_END": "dropdown - Indicates that an index relative to the end of the list should be used to specify the end of the range from which to [https://github.com/google/blockly/wiki/Lists#getting-a-sublist get a sublist]. [[File:Blockly-get-sublist.png]]", - "LISTS_GET_SUBLIST_END_LAST": "dropdown - Indicates that the '''last''' item in the given list should be [https://github.com/google/blockly/wiki/Lists#getting-a-sublist the end of the selected sublist]. [[File:Blockly-get-sublist.png]]", - "LISTS_GET_SUBLIST_TAIL": "{{Optional|Supply translation only if your language requires it. Most do not.}} block text - This appears in the rightmost position ('tail') of the sublist block, as described at [https://github.com/google/blockly/wiki/Lists#getting-a-sublist https://github.com/google/blockly/wiki/Lists#getting-a-sublist]. In English and most other languages, this is the empty string. [[File:Blockly-get-sublist.png]]", - "LISTS_GET_SUBLIST_TOOLTIP": "tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-a-sublist https://github.com/google/blockly/wiki/Lists#getting-a-sublist] for more information. [[File:Blockly-get-sublist.png]]", + "LISTS_GET_SUBLIST_START_FROM_START": "dropdown - Indicates that an index relative to the front of the list should be used to specify the beginning of the range from which to [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-sublist get a sublist]. [[File:Blockly-get-sublist.png]] Note: If {{msg-blockly|ORDINAL_NUMBER_SUFFIX}} is defined, it will automatically appear ''after'' this number (and any other ordinal numbers on this block). See [[Translating:Blockly#Ordinal_numbers]] for more information on ordinal numbers in Blockly.", + "LISTS_GET_SUBLIST_START_FROM_END": "dropdown - Indicates that an index relative to the end of the list should be used to specify the beginning of the range from which to [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-sublist get a sublist].", + "LISTS_GET_SUBLIST_START_FIRST": "dropdown - Indicates that the [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-sublist sublist to extract] should begin with the list's first item.", + "LISTS_GET_SUBLIST_END_FROM_START": "dropdown - Indicates that an index relative to the front of the list should be used to specify the end of the range from which to [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-sublist get a sublist]. [[File:Blockly-get-sublist.png]]", + "LISTS_GET_SUBLIST_END_FROM_END": "dropdown - Indicates that an index relative to the end of the list should be used to specify the end of the range from which to [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-sublist get a sublist]. [[File:Blockly-get-sublist.png]]", + "LISTS_GET_SUBLIST_END_LAST": "dropdown - Indicates that the '''last''' item in the given list should be [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-sublist the end of the selected sublist]. [[File:Blockly-get-sublist.png]]", + "LISTS_GET_SUBLIST_TAIL": "{{Optional|Supply translation only if your language requires it. Most do not.}} block text - This appears in the rightmost position ('tail') of the sublist block, as described at [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-sublist https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-sublist]. In English and most other languages, this is the empty string. [[File:Blockly-get-sublist.png]]", + "LISTS_GET_SUBLIST_TOOLTIP": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-sublist https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-sublist] for more information. [[File:Blockly-get-sublist.png]]", "LISTS_SORT_HELPURL": "{{Optional}} url - Information describing sorting a list.", "LISTS_SORT_TITLE": "Sort as type %1 (numeric or alphabetic) in order %2 (ascending or descending) a list of items %3.\n{{Identical|Sort}}", - "LISTS_SORT_TOOLTIP": "tooltip - See [https://github.com/google/blockly/wiki/Lists#sorting-a-list].", + "LISTS_SORT_TOOLTIP": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#sorting-a-list].", "LISTS_SORT_ORDER_ASCENDING": "sorting order or direction from low to high value for numeric, or A-Z for alphabetic.\n{{Identical|Ascending}}", "LISTS_SORT_ORDER_DESCENDING": "sorting order or direction from high to low value for numeric, or Z-A for alphabetic.\n{{Identical|Descending}}", "LISTS_SORT_TYPE_NUMERIC": "sort by treating each item as a number.", @@ -358,8 +360,8 @@ "LISTS_SPLIT_LIST_FROM_TEXT": "dropdown - Indicates that text will be split up into a list (e.g. 'a-b-c' -> ['a', 'b', 'c']).", "LISTS_SPLIT_TEXT_FROM_LIST": "dropdown - Indicates that a list will be joined together to form text (e.g. ['a', 'b', 'c'] -> 'a-b-c').", "LISTS_SPLIT_WITH_DELIMITER": "block text - Prompts for a letter to be used as a separator when splitting or joining text.", - "LISTS_SPLIT_TOOLTIP_SPLIT": "tooltip - See [https://github.com/google/blockly/wiki/Lists#make-list-from-text https://github.com/google/blockly/wiki/Lists#make-list-from-text] for more information.", - "LISTS_SPLIT_TOOLTIP_JOIN": "tooltip - See [https://github.com/google/blockly/wiki/Lists#make-text-from-list https://github.com/google/blockly/wiki/Lists#make-text-from-list] for more information.", + "LISTS_SPLIT_TOOLTIP_SPLIT": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#make-list-from-text https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#make-list-from-text] for more information.", + "LISTS_SPLIT_TOOLTIP_JOIN": "tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#make-text-from-list https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#make-text-from-list] for more information.", "LISTS_REVERSE_HELPURL": "{{Optional}} url - Information describing reversing a list.", "LISTS_REVERSE_MESSAGE0": "block text - Title of block that returns a copy of a list (%1) with the order of items reversed.", "LISTS_REVERSE_TOOLTIP": "tooltip - Short description for a block that reverses a copy of a list.", @@ -402,5 +404,27 @@ "WORKSPACE_ARIA_LABEL": "workspace - This text is read out when a user navigates to the workspace while using a screen reader.", "COLLAPSED_WARNINGS_WARNING": "warning - This appears if the user collapses a block, and blocks inside that block have warnings attached to them. It should inform the user that the block they collapsed contains blocks that have warnings.", "DIALOG_OK": "button label - Pressing this button closes help information.\n{{Identical|OK}}", - "DIALOG_CANCEL": "button label - Pressing this button cancels a proposed action.\n{{Identical|Cancel}}" + "DIALOG_CANCEL": "button label - Pressing this button cancels a proposed action.\n{{Identical|Cancel}}", + "EDIT_BLOCK_CONTENTS": "menu label - Contextual menu item that moves the keyboard navigation cursor into a subitem of the focused block.", + "MOVE_BLOCK": "menu label - Contextual menu item that starts a keyboard-driven block move.", + "WINDOWS": "Name of the Microsoft Windows operating system displayed in a list of keyboard shortcuts.", + "MAC_OS": "Name of the Apple macOS operating system displayed in a list of keyboard shortcuts,", + "CHROME_OS": "Name of the Google ChromeOS operating system displayed in a list of keyboard shortcuts.", + "LINUX": "Name of the GNU/Linux operating system displayed in a list of keyboard shortcuts.", + "UNKNOWN": "Placeholder name for an operating system that can't be identified in a list of keyboard shortcuts.", + "CONTROL_KEY": "Representation of the Control key used in keyboard shortcuts.", + "COMMAND_KEY": "Representation of the Mac Command key used in keyboard shortcuts.", + "OPTION_KEY": "Representation of the Mac Option key used in keyboard shortcuts.", + "ALT_KEY": "Representation of the Alt key used in keyboard shortcuts.", + "CUT_SHORTCUT": "menu label - Contextual menu item that cuts the focused item.", + "COPY_SHORTCUT": "menu label - Contextual menu item that copies the focused item.", + "PASTE_SHORTCUT": "menu label - Contextual menu item that pastes the previously copied item.", + "HELP_PROMPT": "Alert message shown to prompt users to review available keyboard shortcuts.", + "SHORTCUTS_GENERAL": "shortcut list section header - Label for general purpose keyboard shortcuts.", + "SHORTCUTS_EDITING": "shortcut list section header - Label for keyboard shortcuts related to editing a workspace.", + "SHORTCUTS_CODE_NAVIGATION": "shortcut list section header - Label for keyboard shortcuts related to moving around the workspace.", + "KEYBOARD_NAV_UNCONSTRAINED_MOVE_HINT": "Message shown to inform users how to move blocks to arbitrary locations with the keyboard.", + "KEYBOARD_NAV_CONSTRAINED_MOVE_HINT": "Message shown to inform users how to move blocks with the keyboard.", + "KEYBOARD_NAV_COPIED_HINT": "Message shown when an item is copied in keyboard navigation mode.", + "KEYBOARD_NAV_CUT_HINT": "Message shown when an item is cut in keyboard navigation mode." } diff --git a/msg/json/ro.json b/packages/blockly/msg/json/ro.json similarity index 100% rename from msg/json/ro.json rename to packages/blockly/msg/json/ro.json diff --git a/msg/json/ru.json b/packages/blockly/msg/json/ru.json similarity index 100% rename from msg/json/ru.json rename to packages/blockly/msg/json/ru.json diff --git a/msg/json/sc.json b/packages/blockly/msg/json/sc.json similarity index 100% rename from msg/json/sc.json rename to packages/blockly/msg/json/sc.json diff --git a/msg/json/sco.json b/packages/blockly/msg/json/sco.json similarity index 100% rename from msg/json/sco.json rename to packages/blockly/msg/json/sco.json diff --git a/msg/json/sd.json b/packages/blockly/msg/json/sd.json similarity index 99% rename from msg/json/sd.json rename to packages/blockly/msg/json/sd.json index f6e7b5f030d..a481d744e60 100644 --- a/msg/json/sd.json +++ b/packages/blockly/msg/json/sd.json @@ -19,7 +19,7 @@ "EXTERNAL_INPUTS": "ٻاهريون داخلائون", "INLINE_INPUTS": "اِنلائين اِن پٽس", "DELETE_BLOCK": "بلاڪ ڊاهيو", - "DELETE_X_BLOCKS": "1٪ بلاڪ ڊاهيو", + "DELETE_X_BLOCKS": "%1 بلاڪ ڊاهيو", "DELETE_ALL_BLOCKS": "سڀ %1 بلاڪ ڊاھيون؟", "CLEAN_UP": "بلاڪ صاف ڪيو", "COLLAPSE_BLOCK": "بلاڪ ڍڪيو", diff --git a/msg/json/shn.json b/packages/blockly/msg/json/shn.json similarity index 100% rename from msg/json/shn.json rename to packages/blockly/msg/json/shn.json diff --git a/msg/json/si.json b/packages/blockly/msg/json/si.json similarity index 100% rename from msg/json/si.json rename to packages/blockly/msg/json/si.json diff --git a/msg/json/sk.json b/packages/blockly/msg/json/sk.json similarity index 98% rename from msg/json/sk.json rename to packages/blockly/msg/json/sk.json index ab400afd573..f458f0f8e4d 100644 --- a/msg/json/sk.json +++ b/packages/blockly/msg/json/sk.json @@ -9,6 +9,7 @@ "Marian.stano", "Mark", "Nykta 1917", + "Oujon", "Pmikolas44", "TomášPolonec", "Yardom78" @@ -47,6 +48,7 @@ "NEW_VARIABLE_TITLE": "Názov novej premennej:", "VARIABLE_ALREADY_EXISTS": "Premenná s názvom %1 už existuje.", "VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE": "Premenná s názvom '%1' už existuje pre inú premennú typu '%2'.", + "VARIABLE_ALREADY_EXISTS_FOR_A_PARAMETER": "Premenná s názvom '%1' už existuje ako parameter v procedúre '%2'.", "DELETE_VARIABLE_CONFIRMATION": "Odstrániť %1 použití premennej '%2'?", "CANNOT_DELETE_VARIABLE_PROCEDURE": "Nie je možné zmazať premennú „%1“, pretože je súčasťou definície funkcie „%2“", "DELETE_VARIABLE": "Odstrániť premennú '%1'", @@ -111,6 +113,9 @@ "LOGIC_TERNARY_IF_FALSE": "ak nepravda", "LOGIC_TERNARY_TOOLTIP": "Skontroluj podmienku testom. Ak je podmienka pravda, vráť hodnotu \"ak pravda\", inak vráť hodnotu \"ak nepravda\".", "MATH_NUMBER_TOOLTIP": "Číslo.", + "MATH_TRIG_SIN": "Sin", + "MATH_TRIG_COS": "Cos", + "MATH_TRIG_TAN": "Tan", "MATH_TRIG_ASIN": "arcsin", "MATH_TRIG_ACOS": "arccos", "MATH_TRIG_ATAN": "arctan", @@ -247,6 +252,7 @@ "LISTS_GET_INDEX_GET": "zisti", "LISTS_GET_INDEX_GET_REMOVE": "zisti a odstráň", "LISTS_GET_INDEX_REMOVE": "odstráň", + "LISTS_GET_INDEX_FROM_START": "#", "LISTS_GET_INDEX_FROM_END": "# od konca", "LISTS_GET_INDEX_FIRST": "prvý", "LISTS_GET_INDEX_LAST": "posledný", @@ -306,6 +312,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "urob niečo", "PROCEDURES_BEFORE_PARAMS": "s:", "PROCEDURES_CALL_BEFORE_PARAMS": "s:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Nie je možné spustiť užívateľom definovanú funkciu '%1', pretože blok definície je zakázaný.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Vytvorí funciu bez výstupu.", "PROCEDURES_DEFNORETURN_COMMENT": "Doplň, čo robí táto funkcia...", "PROCEDURES_DEFRETURN_RETURN": "vrátiť", diff --git a/msg/json/skr-arab.json b/packages/blockly/msg/json/skr-arab.json similarity index 100% rename from msg/json/skr-arab.json rename to packages/blockly/msg/json/skr-arab.json diff --git a/msg/json/sl.json b/packages/blockly/msg/json/sl.json similarity index 100% rename from msg/json/sl.json rename to packages/blockly/msg/json/sl.json diff --git a/msg/json/smn.json b/packages/blockly/msg/json/smn.json similarity index 100% rename from msg/json/smn.json rename to packages/blockly/msg/json/smn.json diff --git a/msg/json/sq.json b/packages/blockly/msg/json/sq.json similarity index 100% rename from msg/json/sq.json rename to packages/blockly/msg/json/sq.json diff --git a/msg/json/sr-latn.json b/packages/blockly/msg/json/sr-latn.json similarity index 100% rename from msg/json/sr-latn.json rename to packages/blockly/msg/json/sr-latn.json diff --git a/msg/json/sr.json b/packages/blockly/msg/json/sr.json similarity index 99% rename from msg/json/sr.json rename to packages/blockly/msg/json/sr.json index 4741be7485a..64ffd6559ee 100644 --- a/msg/json/sr.json +++ b/packages/blockly/msg/json/sr.json @@ -1,6 +1,7 @@ { "@metadata": { "authors": [ + "Aca", "Acamicamacaraca", "BadDog", "Kizule", diff --git a/msg/json/sv.json b/packages/blockly/msg/json/sv.json similarity index 98% rename from msg/json/sv.json rename to packages/blockly/msg/json/sv.json index 45c3864d6a8..eebfb269b86 100644 --- a/msg/json/sv.json +++ b/packages/blockly/msg/json/sv.json @@ -189,7 +189,7 @@ "MATH_RANDOM_FLOAT_TOOLTIP": "Ger tillbaka ett slumpat decimaltal mellan 0.0 (inkluderat) och 1.0 (exkluderat).", "MATH_ATAN2_TITLE": "atan2 av X:%1 Y:%2", "MATH_ATAN2_TOOLTIP": "Returnerar arcustangens av punkt (X, Y) i grader från -180 till 180.", - "TEXT_TEXT_HELPURL": "https://sv.wikipedia.org/wiki/Str%C3%A4ng_%28data%29", + "TEXT_TEXT_HELPURL": "https://sv.wikipedia.org/wiki/Str%C3%A4ng_(data)", "TEXT_TEXT_TOOLTIP": "En bokstav, ord eller textrad.", "TEXT_JOIN_TITLE_CREATEWITH": "skapa text med", "TEXT_JOIN_TOOLTIP": "Skapa en textbit genom att sammanfoga ett valfritt antal föremål.", @@ -322,6 +322,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "göra något", "PROCEDURES_BEFORE_PARAMS": "med:", "PROCEDURES_CALL_BEFORE_PARAMS": "med:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Kan inte att köra den användardefinierade funktionen \"%1\" eftersom definitionsblocket är inaktiverat.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Skapar en funktion utan output.", "PROCEDURES_DEFNORETURN_COMMENT": "Beskriv denna funktion...", "PROCEDURES_DEFRETURN_HELPURL": "https://sv.wikipedia.org/wiki/Funktion_(programmering)", @@ -329,7 +330,9 @@ "PROCEDURES_DEFRETURN_TOOLTIP": "Skapar en funktion med output.", "PROCEDURES_ALLOW_STATEMENTS": "tillåta uttalanden", "PROCEDURES_DEF_DUPLICATE_WARNING": "Varning: Denna funktion har dubbla parametrar.", + "PROCEDURES_CALLNORETURN_HELPURL": "https://sv.wikipedia.org/wiki/Funktion_(programmering)", "PROCEDURES_CALLNORETURN_TOOLTIP": "Kör den användardefinierade funktionen \"%1\".", + "PROCEDURES_CALLRETURN_HELPURL": "https://sv.wikipedia.org/wiki/Funktion_(programmering)", "PROCEDURES_CALLRETURN_TOOLTIP": "Kör den användardefinierade funktionen \"%1\" och använd resultatet av den.", "PROCEDURES_MUTATORCONTAINER_TITLE": "inmatningar", "PROCEDURES_MUTATORCONTAINER_TOOLTIP": "Lägg till, ta bort och ändra ordningen för inmatningar till denna funktion.", diff --git a/msg/json/sw.json b/packages/blockly/msg/json/sw.json similarity index 100% rename from msg/json/sw.json rename to packages/blockly/msg/json/sw.json diff --git a/msg/json/synonyms.json b/packages/blockly/msg/json/synonyms.json similarity index 100% rename from msg/json/synonyms.json rename to packages/blockly/msg/json/synonyms.json diff --git a/msg/json/ta.json b/packages/blockly/msg/json/ta.json similarity index 100% rename from msg/json/ta.json rename to packages/blockly/msg/json/ta.json diff --git a/msg/json/tcy.json b/packages/blockly/msg/json/tcy.json similarity index 99% rename from msg/json/tcy.json rename to packages/blockly/msg/json/tcy.json index 06704e50b7b..712f9dfc9ba 100644 --- a/msg/json/tcy.json +++ b/packages/blockly/msg/json/tcy.json @@ -28,7 +28,7 @@ "EXPAND_ALL": "ಮಾತಾ ತಡೆಕ್ಲೆನ ಮಾಹಿತಿನ್ ಪರಡಾವು", "DISABLE_BLOCK": "ಬ್ಲಾಕ್‍ನ್ ದೆತ್ತ್‌ಪಾಡ್", "ENABLE_BLOCK": "ತಡೆನ್ ಸಕ್ರಿಯೊ ಮಲ್ಪು", - "HELP": "ಸಹಾಯೊ", + "HELP": "ಸಕಾಯೊ", "UNDO": "ದುಂಬುದಲೆಕೊ", "REDO": "ಕುಡ ಮಲ್ಪು", "CHANGE_VALUE_TITLE": "ಮೌಲ್ಯೊನು ಬದಲ್ ಮಲ್ಪು", @@ -311,6 +311,6 @@ "PROCEDURES_IFRETURN_TOOLTIP": "ಮೌಲ್ಯೊ ಸತ್ಯೊ ಆಂಡ, ರಡ್ಡನೆ ಮೌಲ್ಯೊನು ಪಿರಕೊರು.", "PROCEDURES_IFRETURN_WARNING": "ಎಚ್ಚರಿಕೆ: ಒಂಜಿ ಕಾರ್ಯ ವ್ಯಾಕ್ಯಾನೊದುಲಯಿ ಮಾತ್ರ ಈ ತಡೆನ್ ಗಲಸೊಲಿ.", "WORKSPACE_COMMENT_DEFAULT_TEXT": "ದಾದಾಂಡಲ ಪನ್ಲೇ...", - "DIALOG_OK": "ಅವು", + "DIALOG_OK": "ಆವು", "DIALOG_CANCEL": "ಉಂತಾಲೆ" } diff --git a/msg/json/tdd.json b/packages/blockly/msg/json/tdd.json similarity index 99% rename from msg/json/tdd.json rename to packages/blockly/msg/json/tdd.json index 2bb756d2367..2f63c5787de 100644 --- a/msg/json/tdd.json +++ b/packages/blockly/msg/json/tdd.json @@ -2,6 +2,7 @@ "@metadata": { "authors": [ "AeyTaiNuea", + "Dai Meng Mao Long", "咽頭べさ" ] }, diff --git a/msg/json/te.json b/packages/blockly/msg/json/te.json similarity index 100% rename from msg/json/te.json rename to packages/blockly/msg/json/te.json diff --git a/msg/json/th.json b/packages/blockly/msg/json/th.json similarity index 100% rename from msg/json/th.json rename to packages/blockly/msg/json/th.json diff --git a/msg/json/ti.json b/packages/blockly/msg/json/ti.json similarity index 100% rename from msg/json/ti.json rename to packages/blockly/msg/json/ti.json diff --git a/msg/json/tl.json b/packages/blockly/msg/json/tl.json similarity index 85% rename from msg/json/tl.json rename to packages/blockly/msg/json/tl.json index fad9ab9f91c..1eb04100699 100644 --- a/msg/json/tl.json +++ b/packages/blockly/msg/json/tl.json @@ -1,23 +1,43 @@ { "@metadata": { "authors": [ + "GinawaSaHapon", "아라" ] }, - "DUPLICATE_BLOCK": "Kaparehas", - "ADD_COMMENT": "Dagdag komento", - "EXTERNAL_INPUTS": "Panlabas na Inputs", - "INLINE_INPUTS": "Inline na Inputs", - "DELETE_BLOCK": "burahin ang bloke", - "DELETE_X_BLOCKS": "burahin %1 ng bloke", - "COLLAPSE_BLOCK": "bloke", - "COLLAPSE_ALL": "bloke", - "EXPAND_BLOCK": "Palawakin ang Block", - "EXPAND_ALL": "Palawakin ang Blocks", - "DISABLE_BLOCK": "Ipangwalang bisa ang Block", - "ENABLE_BLOCK": "Bigyan ng bisa ang Block", + "VARIABLES_DEFAULT_NAME": "item", + "UNNAMED_KEY": "walang pangalan", + "TODAY": "Ngayon", + "DUPLICATE_BLOCK": "I-duplicate", + "ADD_COMMENT": "Magkomento", + "REMOVE_COMMENT": "Alisin ang Komento", + "DUPLICATE_COMMENT": "I-duplicate ang Komento", + "EXTERNAL_INPUTS": "Input sa Labas", + "INLINE_INPUTS": "Input sa Linya", + "DELETE_BLOCK": "Alisin ang Block", + "DELETE_X_BLOCKS": "Alisin ang %1 (na) Block", + "DELETE_ALL_BLOCKS": "Alisin ang %1 (na) block?", + "CLEAN_UP": "Ayusin ang mga Block", + "COLLAPSE_BLOCK": "Itago ang Block", + "COLLAPSE_ALL": "Itago ang mga Block", + "EXPAND_BLOCK": "Buksan ang Block", + "EXPAND_ALL": "Buksan ang mga Block", + "DISABLE_BLOCK": "I-disable ang Block", + "ENABLE_BLOCK": "I-enable ang Block", "HELP": "Tulong", - "CHANGE_VALUE_TITLE": "pagbago ng value:", + "UNDO": "I-undo", + "REDO": "I-redo", + "CHANGE_VALUE_TITLE": "Baguhin ang value:", + "RENAME_VARIABLE": "I-rename ang variable...", + "RENAME_VARIABLE_TITLE": "I-rename ang lahat ng mga '%1' na variable bilang:", + "NEW_VARIABLE": "Gumawa ng variable...", + "NEW_STRING_VARIABLE": "Gumawa ng string variable...", + "NEW_NUMBER_VARIABLE": "Gumawa ng number variable...", + "NEW_COLOUR_VARIABLE": "Gumawa ng color variable...", + "NEW_VARIABLE_TYPE_TITLE": "Uri ng variable:", + "NEW_VARIABLE_TITLE": "Pangalan ng variable:", + "VARIABLE_ALREADY_EXISTS": "Meron na'ng variable na '%1'.", + "VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE": "Meron na'ng variable na '%1' sa ibang uri: '%2'", "COLOUR_PICKER_TOOLTIP": "pagpili ng kulay sa paleta.", "COLOUR_RANDOM_TITLE": "iba ibang kulay", "COLOUR_RANDOM_TOOLTIP": "pagpili ng iba't ibang kulay.", diff --git a/msg/json/tlh.json b/packages/blockly/msg/json/tlh.json similarity index 100% rename from msg/json/tlh.json rename to packages/blockly/msg/json/tlh.json diff --git a/msg/json/tr.json b/packages/blockly/msg/json/tr.json similarity index 99% rename from msg/json/tr.json rename to packages/blockly/msg/json/tr.json index b65a5ce31f7..bec55f10263 100644 --- a/msg/json/tr.json +++ b/packages/blockly/msg/json/tr.json @@ -342,6 +342,7 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "bir şey yap", "PROCEDURES_BEFORE_PARAMS": "ile:", "PROCEDURES_CALL_BEFORE_PARAMS": "ile:", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "Kullanıcı tanımlı '%1' işlevi, tanımlama bloğu devre dışı olduğundan çalıştırılamıyor.", "PROCEDURES_DEFNORETURN_TOOLTIP": "Çıkışı olmayan bir işlev oluşturur.", "PROCEDURES_DEFNORETURN_COMMENT": "Bu işlevi açıklayın...", "PROCEDURES_DEFRETURN_HELPURL": "https://tr.wikipedia.org/wiki/Altyordam", diff --git a/msg/json/ug-arab.json b/packages/blockly/msg/json/ug-arab.json similarity index 67% rename from msg/json/ug-arab.json rename to packages/blockly/msg/json/ug-arab.json index 37dd257a3f7..07af758f3ba 100644 --- a/msg/json/ug-arab.json +++ b/packages/blockly/msg/json/ug-arab.json @@ -2,15 +2,18 @@ "@metadata": { "authors": [ "HushBeg", + "Nail123Real", "Uzdil", "چۈشكۈن" ] }, "VARIABLES_DEFAULT_NAME": "تۈر", + "UNNAMED_KEY": "نامسىز", "TODAY": "بۈگۈن", "DUPLICATE_BLOCK": "كۆچۈرۈش", "ADD_COMMENT": "ئىزاھات قوشۇش", "REMOVE_COMMENT": "ئىزاھاتنى ئۆچۈرۈش", + "DUPLICATE_COMMENT": "تەكرار باھا", "EXTERNAL_INPUTS": "سىرتقى كىرگۈزۈش", "INLINE_INPUTS": "تاق قۇرلۇق كىرگۈزۈش", "DELETE_BLOCK": "بۆلەك ئۆچۈرۈش", @@ -30,10 +33,16 @@ "RENAME_VARIABLE": "ئۆزگەرگۈچى مىقدارغا قايتا نام قويۇش", "RENAME_VARIABLE_TITLE": "بارلىق بۆلەك “%1\" ئۆزگەرگۈچى مىقدار قايتا ناملىنىپ :", "NEW_VARIABLE": "ئۆزگەرگۈچى مىقدار ... قۇرۇش", + "NEW_STRING_VARIABLE": "قۇر ئۆزگەرگۈچى مىقدار قۇر...", + "NEW_NUMBER_VARIABLE": "سان ئۆزگەرگۈچى مىقدار قۇر...", + "NEW_COLOUR_VARIABLE": "سان ئۆزگەرگۈچى مىقدار قۇر...", + "NEW_VARIABLE_TYPE_TITLE": "يېڭى ئۆزگەرگۈچى تىپى:", "NEW_VARIABLE_TITLE": "يېڭى ئۆزگەرگۈچى مىقدار نامى:", "VARIABLE_ALREADY_EXISTS": "ئىسم مەۋجۇت “%1” ئۆزگەرگۈچى", "VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE": "ئىسىملىك“%1” ئۆزگەرگۈچى مىقدار مەۋجۇت بولۇپ تۇرىدۇ ، لېكىن يەنە بىر ئۆزگەرگۈچى مىقدار تىپى بولۇش سۈپىتى بىلەن “%2” مەۋجۇت .", + "VARIABLE_ALREADY_EXISTS_FOR_A_PARAMETER": "'%1' دېگەن ئۆزگەرگۈچى مىقدار '%2' تەرتىپىدە پارامېتىر سۈپىتىدە مەۋجۇت.", "DELETE_VARIABLE_CONFIRMATION": "ئۆچۈرۈش “%2” ئۆزگەرگۈچى مىقدار%1 ئىشلىتىلىش ئورنى بارمۇ؟", + "CANNOT_DELETE_VARIABLE_PROCEDURE": "ئۆزگەرگۈچى مىقدار '%1' نى ئۆچۈرەلمەيدۇ ، چۈنكى ئۇ '%2' فۇنكىسىيەسىنىڭ ئېنىقلىمىسىنىڭ بىر قىسمى.", "DELETE_VARIABLE": "“%1” ئۆزگەرگۈچى مىقدارنى ئۆچۈرۈش", "COLOUR_PICKER_HELPURL": "https://zh.wikipedia.org/wiki/رەڭگى", "COLOUR_PICKER_TOOLTIP": "تاختىدىن رەڭنى تاللاڭ", @@ -43,15 +52,24 @@ "COLOUR_RGB_RED": "قىزىل", "COLOUR_RGB_GREEN": "يېشىل", "COLOUR_RGB_BLUE": "كۆك", + "COLOUR_RGB_TOOLTIP": "بەلگىلەنگەن مىقداردا قىزىل ، يېشىل ۋە كۆك رەڭ ھاسىل قىلىڭ. بارلىق قىممەتلەر چوقۇم 0 دىن 100 گىچە بولۇشى كېرەك.", "COLOUR_BLEND_TITLE": "ئارىلاش", "COLOUR_BLEND_COLOUR1": "رەڭ 1", "COLOUR_BLEND_COLOUR2": "رەڭ 2", "COLOUR_BLEND_RATIO": "نىسبەت", + "COLOUR_BLEND_TOOLTIP": "ئىككى خىل رەڭنى مەلۇم نىسبەت بىلەن ئارىلاشتۇرۇڭ (0.0 - 1.0).", "CONTROLS_REPEAT_HELPURL": "https://zh.wikipedia.org/wiki/Forئايلىنىش", "CONTROLS_REPEAT_TITLE": "تەكرار %1قېتىم", "CONTROLS_REPEAT_INPUT_DO": "ئىجرا", + "CONTROLS_REPEAT_TOOLTIP": "بەزى بايانلارنى بىر نەچچە قېتىم قىلىڭ.", "CONTROLS_WHILEUNTIL_OPERATOR_WHILE": "تەكرار بولۇش", "CONTROLS_WHILEUNTIL_OPERATOR_UNTIL": "تەكرارلىقى", + "CONTROLS_WHILEUNTIL_TOOLTIP_WHILE": "بىر قىممەت راست بولسىمۇ ، بەزى بايانلارنى قىلىڭ.", + "CONTROLS_WHILEUNTIL_TOOLTIP_UNTIL": "قىممەت يالغان بولسىمۇ ، بەزى بايانلارنى قىلىڭ.", + "CONTROLS_FOR_TOOLTIP": "ئۆزگەرگۈچى مىقدار '%1' باشلىنىش نومۇرىدىن ئاخىرقى سانغىچە بولغان قىممەتنى ئېلىپ ، بەلگىلەنگەن ئارىلىقنى ھېسابلاپ ، كۆرسىتىلگەن بۆلەكلەرنى قىلىڭ.", + "CONTROLS_FOR_TITLE": "%1 بىلەن%2 دىن%3 گىچە ھېسابلاڭ %4", + "CONTROLS_FOREACH_TITLE": "ھەر بىر تۈر ئۈچۈن%1 تىزىملىكتىكى%2", + "CONTROLS_FOREACH_TOOLTIP": "تىزىملىكتىكى ھەر بىر تۈرگە ئۆزگەرگۈچى مىقدارنى «%1» قىلىپ تەڭشەڭ ، ئاندىن بەزى بايانلارنى قىلىڭ.", "CONTROLS_FLOW_STATEMENTS_OPERATOR_BREAK": "ئۈزۈلۈپ ئايلىنىش", "CONTROLS_FLOW_STATEMENTS_OPERATOR_CONTINUE": "كىيىنكى قېتىم داۋاملىق ئايلىنىشن", "CONTROLS_FLOW_STATEMENTS_TOOLTIP_BREAK": "ئۇنىڭ دەۋرىي ئۈزۈلۈش ئۆز ئىچىگە ئالىدۇ .", @@ -74,9 +92,12 @@ "LOGIC_COMPARE_TOOLTIP_LTE": "ئەگەر تۇنجى كىرگۈزۈش نەتىجىسى ئىككىنچى كىرگۈزۈش نەتىجىسى تىن تۆۋەن ياكى شۇنىڭغا تەڭ بولسا راستىنلا كەينىگە قايتسا .", "LOGIC_COMPARE_TOOLTIP_GT": "ئەگەر تۇنجى كىرگۈزۈش نەتىجىسى ئىشككىنچى چوڭ بولسا راستىنلا كەينىگە قايتسا .", "LOGIC_COMPARE_TOOLTIP_GTE": "ئەگەر تۇنجى كىرگۈزۈش نەتىجىدە ئىشككىنچى كىچىك بولسا راستىنلا كەينىگە قايتسا .", + "LOGIC_OPERATION_TOOLTIP_AND": "ئەگەر ھەر ئىككى كىرگۈزۈش توغرا بولسا ، ھەقىقىي قايتىڭ.", "LOGIC_OPERATION_AND": "ۋە", + "LOGIC_OPERATION_TOOLTIP_OR": "كەم دېگەندە كىرگۈزۈشنىڭ بىرى راست بولسا ھەقىقىي قايتىڭ.", "LOGIC_OPERATION_OR": "ياكى", "LOGIC_NEGATE_TITLE": "ئەمەس%1", + "LOGIC_NEGATE_TOOLTIP": "ئەگەر كىرگۈزۈش يالغان بولسا توغرا قايتىدۇ. ئەگەر كىرگۈزۈش راست بولسا يالغاننى قايتۇرىدۇ.", "LOGIC_BOOLEAN_TRUE": "ھەقىقىي", "LOGIC_BOOLEAN_FALSE": "يالغان", "LOGIC_BOOLEAN_TOOLTIP": "راست ياكى يالغان قايتىش", @@ -85,12 +106,31 @@ "LOGIC_TERNARY_CONDITION": "سىناق", "LOGIC_TERNARY_IF_TRUE": "ئەگەر راست بولسا", "LOGIC_TERNARY_IF_FALSE": "ئەگەر يالغان بولسا", + "LOGIC_TERNARY_TOOLTIP": "«سىناق» دىكى ئەھۋالنى تەكشۈرۈڭ. ئەگەر شەرت راست بولسا ، 'if true' قىممىتىنى قايتۇرىدۇ. بولمىسا 'if false' قىممىتىنى قايتۇرىدۇ.", "MATH_NUMBER_HELPURL": "https://zh.wikipedia.org/wiki/سان", "MATH_NUMBER_TOOLTIP": "بىر سان.", + "MATH_TRIG_SIN": "گۇناھ", + "MATH_TRIG_COS": "cos", + "MATH_TRIG_TAN": "tan", + "MATH_TRIG_ASIN": "گۇناھ", + "MATH_TRIG_ACOS": "acos", + "MATH_TRIG_ATAN": "atan", "MATH_ARITHMETIC_HELPURL": "https://zh.wikipedia.org/wiki/ئارىفمېتىكىلىق", + "MATH_ARITHMETIC_TOOLTIP_ADD": "ئىككى ساننىڭ يىغىندىسىنى قايتۇرۇڭ.", + "MATH_ARITHMETIC_TOOLTIP_MINUS": "ئىككى ساننىڭ پەرقىنى قايتۇرۇڭ.", + "MATH_ARITHMETIC_TOOLTIP_MULTIPLY": "ئىككى ساننىڭ مەھسۇلاتىنى قايتۇرۇڭ.", + "MATH_ARITHMETIC_TOOLTIP_DIVIDE": "ئىككى ساننىڭ سانىنى قايتۇرۇڭ.", + "MATH_ARITHMETIC_TOOLTIP_POWER": "بىرىنچى ساننى ئىككىنچى ساننىڭ كۈچىگە قايتۇرۇڭ.", "MATH_SINGLE_HELPURL": "https://zh.wikipedia.org/wiki/كۋادرات يىلتىز", "MATH_SINGLE_OP_ROOT": "كۋادرات يىلتىز", + "MATH_SINGLE_TOOLTIP_ROOT": "ساننىڭ كۋادرات يىلتىزىنى قايتۇرۇڭ.", "MATH_SINGLE_OP_ABSOLUTE": "مۇتلەق", + "MATH_SINGLE_TOOLTIP_ABS": "بىر ساننىڭ مۇتلەق قىممىتىنى قايتۇرۇڭ.", + "MATH_SINGLE_TOOLTIP_NEG": "بىر ساننىڭ رەت قىلىنىشىنى قايتۇرۇڭ.", + "MATH_SINGLE_TOOLTIP_LN": "بىر ساننىڭ تەبىئىي لوگارىزىمنى قايتۇرۇڭ.", + "MATH_SINGLE_TOOLTIP_LOG10": "بىر ساننىڭ ئاساسى 10 لوگارىزىمنى قايتۇرۇڭ.", + "MATH_SINGLE_TOOLTIP_EXP": "e نى ساننىڭ كۈچىگە قايتۇرۇڭ.", + "MATH_SINGLE_TOOLTIP_POW10": "10 نى ساننىڭ كۈچىگە قايتۇرۇڭ.", "MATH_TRIG_HELPURL": "https://zh.wikipedia.org/wiki/ترىگونومېتىرىيىلىك فۇنكسىيە", "MATH_CONSTANT_HELPURL": "https://zh.wikipedia.org/wiki/ماتېماتىكا تۇراقلىق سانى", "MATH_IS_EVEN": "جۈپ سان", @@ -106,6 +146,7 @@ "MATH_ROUND_OPERATOR_ROUND": "تۆۋەنگە تارتىڭ", "MATH_ROUND_OPERATOR_ROUNDUP": "تۆۋەنگە تارتىڭ", "MATH_ROUND_OPERATOR_ROUNDDOWN": "تۆۋەنگە تارتىڭ", + "MATH_ONLIST_OPERATOR_SUM": "تىزىملىكنىڭ يىغىندىسى", "MATH_ONLIST_OPERATOR_MIN": "جەدۋەل ئىچىدىكى ئەڭ كىچىك قىممەت", "MATH_ONLIST_TOOLTIP_MIN": "جەدۋەلدىكى ئەڭ كىچىك سانغا قايتىش", "MATH_ONLIST_OPERATOR_MAX": "جەدۋەلدىكى ئەڭ چوڭ قىممەت", @@ -115,18 +156,25 @@ "MATH_ONLIST_OPERATOR_MODE": "جەدۋەل ھالىتى", "MATH_MODULO_HELPURL": "https://zh.wikipedia.org/wiki/مودېل ھېسابى", "TEXT_CREATE_JOIN_TITLE_JOIN": "قوشۇش", + "TEXT_PRINT_TITLE": "بېسىپ چىقىرىش %1", + "TEXT_PRINT_TOOLTIP": "كۆرسىتىلگەن تېكىست ، سان ياكى باشقا قىممەتنى بېسىڭ.", + "LISTS_INLIST": "تىزىملىكتە", "LISTS_GET_INDEX_GET": "قولغا كەلتۈرۈش", "LISTS_GET_INDEX_REMOVE": "چىقىرىۋىتىش", + "LISTS_GET_INDEX_FROM_START": "#", + "LISTS_GET_INDEX_FROM_END": "# ئاخىرىدىن", "LISTS_GET_INDEX_FIRST": "تۇنجى", "LISTS_GET_INDEX_LAST": "ئاخىرقى", "LISTS_GET_INDEX_RANDOM": "خالىغانچە", "LISTS_SET_INDEX_SET": "تەڭشەك", "LISTS_SET_INDEX_INSERT": "قىستۇرۇڭ", + "LISTS_SET_INDEX_INPUT_TO": "دېگەندەك", "LISTS_SORT_ORDER_ASCENDING": "يۇقىرىغا", "LISTS_SORT_ORDER_DESCENDING": "تۆۋەنگە", "LISTS_SORT_TYPE_NUMERIC": "سان بويىچە تىزىل", "LISTS_SORT_TYPE_TEXT": "ھەرپ بويىچە تىزىل", "LISTS_SORT_TYPE_IGNORECASE": "ھەرب بويىچە تىزىل، چوڭ كىچىك يېزىلىش ھېساپ قىلىنمايدۇ", + "PROCEDURES_DEFNORETURN_TITLE": "غا", "DIALOG_OK": "ماقۇل", "DIALOG_CANCEL": "ۋاز كەچ" } diff --git a/msg/json/uk.json b/packages/blockly/msg/json/uk.json similarity index 99% rename from msg/json/uk.json rename to packages/blockly/msg/json/uk.json index 6d9b4b45460..d880c758f07 100644 --- a/msg/json/uk.json +++ b/packages/blockly/msg/json/uk.json @@ -4,6 +4,7 @@ "Andriykopanytsia", "Base", "Gzhegozh", + "Ignatgg", "Igor Zavadsky", "Lxlalexlxl", "Movses", diff --git a/msg/json/ur.json b/packages/blockly/msg/json/ur.json similarity index 99% rename from msg/json/ur.json rename to packages/blockly/msg/json/ur.json index adc8bb4859f..b97806609c3 100644 --- a/msg/json/ur.json +++ b/packages/blockly/msg/json/ur.json @@ -1,6 +1,7 @@ { "@metadata": { "authors": [ + "Aafi", "Abdulq", "NajeebKhan", "Obaid Raza", diff --git a/msg/json/uz.json b/packages/blockly/msg/json/uz.json similarity index 100% rename from msg/json/uz.json rename to packages/blockly/msg/json/uz.json diff --git a/msg/json/vi.json b/packages/blockly/msg/json/vi.json similarity index 100% rename from msg/json/vi.json rename to packages/blockly/msg/json/vi.json diff --git a/msg/json/xmf.json b/packages/blockly/msg/json/xmf.json similarity index 93% rename from msg/json/xmf.json rename to packages/blockly/msg/json/xmf.json index c1ae0d980c2..3026a53355a 100644 --- a/msg/json/xmf.json +++ b/packages/blockly/msg/json/xmf.json @@ -38,7 +38,9 @@ "NEW_VARIABLE_TITLE": "ახალი მათირეფონიშ ჯოხო:", "VARIABLE_ALREADY_EXISTS": "მათირეფონი ჯოხოთი '%1' უკვე რე.", "VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE": "მათირეფონი ჯოხოთი '%1' უკვე რე შხვა ტიპიშო: '%2'.", + "VARIABLE_ALREADY_EXISTS_FOR_A_PARAMETER": "მათირე ჯოხოთ '%1' უკვე რე პარამეტრიშ სახეთ პროცედურას '%2'.", "DELETE_VARIABLE_CONFIRMATION": "'%2' მათირეფონიშ გჷმორინაფა %1 ბლასათო?", + "CANNOT_DELETE_VARIABLE_PROCEDURE": "ვეშილებე მათირეშ '%1' ლასუა, ანდანე თინა ფუნქციაშ '%2' გოთანჯუაშ ნორთი რე", "DELETE_VARIABLE": "'%1' მათირეფონიშ ლასუა", "COLOUR_PICKER_HELPURL": "https://xmf.wikipedia.org/wiki/ფერი", "COLOUR_PICKER_TOOLTIP": "გეგშაგორით ფერი პალიტრაშე.", @@ -63,6 +65,7 @@ "CONTROLS_WHILEUNTIL_TOOLTIP_WHILE": "სოიშახ შანულობა ნანდული რე, ზოჯუეფიშ რსულება.", "CONTROLS_WHILEUNTIL_TOOLTIP_UNTIL": "სოიშახ შანულობა ტყურა რე, ზოჯუეფიშ რსულება", "CONTROLS_FOR_TOOLTIP": "მათირეფონი '%1'-ის მითმურჩქინანს შანულობას მოჩამილი ბიჯგეფით დუდშე ბოლოშა დო მეწურაფილ ზოჯუეფს არსულენს.", + "CONTROLS_FOREACH_TITLE": "ირი ელემენტიშო %1 ერკებულს %2", "CONTROLS_FLOW_STATEMENTS_OPERATOR_BREAK": "ციკლშე გიშულა", "CONTROLS_FLOW_STATEMENTS_OPERATOR_CONTINUE": "ციკლიშ გეჸვენჯი ბიჯგშა გინულა", "CONTROLS_FLOW_STATEMENTS_TOOLTIP_BREAK": "თე ციკლიშ მეჭყორიდუა.", diff --git a/msg/json/yo.json b/packages/blockly/msg/json/yo.json similarity index 100% rename from msg/json/yo.json rename to packages/blockly/msg/json/yo.json diff --git a/msg/json/zgh.json b/packages/blockly/msg/json/zgh.json similarity index 94% rename from msg/json/zgh.json rename to packages/blockly/msg/json/zgh.json index 33401304c15..317cf3c17fa 100644 --- a/msg/json/zgh.json +++ b/packages/blockly/msg/json/zgh.json @@ -22,8 +22,8 @@ "RENAME_VARIABLE": "ⵙⵏⴼⵍ ⵉⵙⵎ ⵏ ⵓⵎⵙⴽⵉⵍ...", "RENAME_VARIABLE_TITLE": "ⵙⵏⴼⵍ ⵉⵎⵙⴽⵉⵍⵏ ⴰⴽⴽ '%1' ⵖⵔ:", "NEW_VARIABLE": "ⵙⴽⵔ ⴰⵎⵙⴽⵉⵍ...", - "NEW_NUMBER_VARIABLE": "ⵙⴽⵔ ⴰⵎⴹⴰⵏ ⴰⵎⵙⴽⵉⵍ...", - "NEW_COLOUR_VARIABLE": "ⵙⴽⵔ ⴰⴽⵍⵓ ⴰⵎⵙⴽⵉⵍ...", + "NEW_NUMBER_VARIABLE": "ⵙⵏⵓⵍⴼⵓ ⴰⵎⵙⴽⵉⵍ ⴰⵏⵎⴹⴰⵏ...", + "NEW_COLOUR_VARIABLE": "ⵙⵏⵓⵍⴼⵓ ⴰⵎⵙⴽⵉⵍ ⵏ ⵓⴽⵍⵓ...", "NEW_VARIABLE_TYPE_TITLE": "ⴰⵏⴰⵡ ⴰⵎⴰⵢⵏⵓ ⵏ ⵓⵎⵙⴽⵉⵍ:", "NEW_VARIABLE_TITLE": "ⵉⵙⵎ ⵏ ⵓⵎⵙⴽⵉⵍ ⴰⵎⴰⵢⵏⵓ:", "DELETE_VARIABLE": "ⴽⴽⵙ ⴰⵎⵙⴽⵉⵍ '%1'", @@ -57,7 +57,7 @@ "MATH_CHANGE_TITLE": "ⵙⵏⴼⵍ %1 ⵙ %2", "MATH_CHANGE_TOOLTIP": "ⵔⵏⵓ ⵢⴰⵏ ⵓⵎⴹⴰⵏ ⵖⵔ ⵓⵎⵙⴽⵉⵍ '%1'", "MATH_ATAN2_TITLE": "atan2 ⵙⴳ X:%1 Y:%2", - "TEXT_JOIN_TITLE_CREATEWITH": "ⵙⵏⴼⵍⵓⵍ ⴰⴹⵕⵉⵚ ⵙ", + "TEXT_JOIN_TITLE_CREATEWITH": "ⵙⵏⵓⵍⴼⵓ ⴰⴹⵕⵉⵚ ⵙ", "TEXT_CREATE_JOIN_TITLE_JOIN": "ⵍⴽⵎ", "TEXT_LENGTH_TITLE": "ⵜⵉⵖⵣⵉ ⵏ %1", "TEXT_INDEXOF_TITLE": "ⴳ ⵓⴹⵕⵉⵚ %1 %2 %3", diff --git a/msg/json/zh-hans.json b/packages/blockly/msg/json/zh-hans.json similarity index 98% rename from msg/json/zh-hans.json rename to packages/blockly/msg/json/zh-hans.json index 7bb6b4d50a9..ccbe7733781 100644 --- a/msg/json/zh-hans.json +++ b/packages/blockly/msg/json/zh-hans.json @@ -21,6 +21,7 @@ "Qiyue2001", "Shatteredwind", "Shimamura Sakura", + "TFX202X", "Tonylianlong", "WindWood", "Xiaomingyan", @@ -345,11 +346,12 @@ "PROCEDURES_DEFNORETURN_PROCEDURE": "做点什么", "PROCEDURES_BEFORE_PARAMS": "与:", "PROCEDURES_CALL_BEFORE_PARAMS": "与:", - "PROCEDURES_DEFNORETURN_TOOLTIP": "创建一个不带输出值的函数。", + "PROCEDURES_CALL_DISABLED_DEF_WARNING": "无法运行用户定义函数'%1',因为定义块已被禁用。", + "PROCEDURES_DEFNORETURN_TOOLTIP": "创建没有输出值的函数。", "PROCEDURES_DEFNORETURN_COMMENT": "描述该功能...", "PROCEDURES_DEFRETURN_HELPURL": "https://zh.wikipedia.org/wiki/子程序", "PROCEDURES_DEFRETURN_RETURN": "返回", - "PROCEDURES_DEFRETURN_TOOLTIP": "创建一个有输出值的函数。", + "PROCEDURES_DEFRETURN_TOOLTIP": "创建有输出值的函数。", "PROCEDURES_ALLOW_STATEMENTS": "允许声明", "PROCEDURES_DEF_DUPLICATE_WARNING": "警告:此函数具有重复参数。", "PROCEDURES_CALLNORETURN_HELPURL": "https://zh.wikipedia.org/wiki/子程序", diff --git a/msg/json/zh-hant.json b/packages/blockly/msg/json/zh-hant.json similarity index 100% rename from msg/json/zh-hant.json rename to packages/blockly/msg/json/zh-hant.json diff --git a/msg/messages.js b/packages/blockly/msg/messages.js similarity index 70% rename from msg/messages.js rename to packages/blockly/msg/messages.js index 6b9d663a68b..0cc4d3be455 100644 --- a/msg/messages.js +++ b/packages/blockly/msg/messages.js @@ -85,10 +85,10 @@ Blockly.Msg.REMOVE_COMMENT = 'Remove Comment'; /// context menu - Make a copy of the selected workspace comment.\n{{Identical|Duplicate}} Blockly.Msg.DUPLICATE_COMMENT = 'Duplicate Comment'; /** @type {string} */ -/// context menu - Change from 'external' to 'inline' mode for displaying blocks used as inputs to the selected block. See [[Translating:Blockly#context_menus]]. +/// context menu - Change from 'external' to 'inline' mode for displaying blocks used as inputs to the selected block. See [[Translating:Blockly#context_menus]].\n\nThe opposite of {{msg-blockly|INLINE INPUTS}}. Blockly.Msg.EXTERNAL_INPUTS = 'External Inputs'; /** @type {string} */ -/// context menu - Change from 'internal' to 'external' mode for displaying blocks used as inputs to the selected block. See [[Translating:Blockly#context_menus]]. +/// context menu - Change from 'internal' to 'external' mode for displaying blocks used as inputs to the selected block. See [[Translating:Blockly#context_menus]].\n\nThe opposite of {{msg-blockly|EXTERNAL INPUTS}}. Blockly.Msg.INLINE_INPUTS = 'Inline Inputs'; /** @type {string} */ /// context menu - Permanently delete the selected block. @@ -103,6 +103,9 @@ Blockly.Msg.DELETE_ALL_BLOCKS = 'Delete all %1 blocks?'; /// context menu - Reposition all the blocks so that they form a neat line. Blockly.Msg.CLEAN_UP = 'Clean up Blocks'; /** @type {string} */ +/// toast notification - Accessibility label for close button. +Blockly.Msg.CLOSE = 'Close'; +/** @type {string} */ /// context menu - Make the appearance of the selected block smaller by hiding some information about it. Blockly.Msg.COLLAPSE_BLOCK = 'Collapse Block'; /** @type {string} */ @@ -135,10 +138,10 @@ Blockly.Msg.REDO = 'Redo'; /// prompt - This message is seen on mobile devices and the Opera browser. With most browsers, users can edit numeric values in blocks by just clicking and typing. Opera does not allow this and mobile browsers may have issues with in-line textareas. So we prompt users with this message (usually a popup) to change a value. Blockly.Msg.CHANGE_VALUE_TITLE = 'Change value:'; /** @type {string} */ -/// dropdown choice - When the user clicks on a variable block, this is one of the dropdown menu choices. It is used to rename the current variable. See [https://github.com/google/blockly/wiki/Variables#dropdown-menu https://github.com/google/blockly/wiki/Variables#dropdown-menu]. +/// dropdown choice - When the user clicks on a variable block, this is one of the dropdown menu choices. It is used to rename the current variable. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Variables#dropdown-menu https://github.com/RaspberryPiFoundation/blockly/wiki/Variables#dropdown-menu]. Blockly.Msg.RENAME_VARIABLE = 'Rename variable...'; /** @type {string} */ -/// prompt - Prompts the user to enter the new name for the selected variable. See [https://github.com/google/blockly/wiki/Variables#dropdown-menu https://github.com/google/blockly/wiki/Variables#dropdown-menu].\n\nParameters:\n* %1 - the name of the variable to be renamed. +/// prompt - Prompts the user to enter the new name for the selected variable. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Variables#dropdown-menu https://github.com/RaspberryPiFoundation/blockly/wiki/Variables#dropdown-menu].\n\nParameters:\n* %1 - the name of the variable to be renamed. Blockly.Msg.RENAME_VARIABLE_TITLE = 'Rename all "%1" variables to:'; // Variable creation @@ -158,7 +161,7 @@ Blockly.Msg.NEW_COLOUR_VARIABLE = 'Create colour variable...'; /// prompt - Prompts the user to enter the type for a variable. Blockly.Msg.NEW_VARIABLE_TYPE_TITLE = 'New variable type:'; /** @type {string} */ -/// prompt - Prompts the user to enter the name for a new variable. See [https://github.com/google/blockly/wiki/Variables#dropdown-menu https://github.com/google/blockly/wiki/Variables#dropdown-menu]. +/// prompt - Prompts the user to enter the name for a new variable. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Variables#dropdown-menu https://github.com/RaspberryPiFoundation/blockly/wiki/Variables#dropdown-menu]. Blockly.Msg.NEW_VARIABLE_TITLE = 'New variable name:'; /** @type {string} */ /// alert - Tells the user that the name they entered is already in use. @@ -186,7 +189,7 @@ Blockly.Msg.DELETE_VARIABLE = 'Delete the "%1" variable'; /// {{Optional}} url - Information about colour. Blockly.Msg.COLOUR_PICKER_HELPURL = 'https://en.wikipedia.org/wiki/Color'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Colour#picking-a-colour-from-a-palette https://github.com/google/blockly/wiki/Colour#picking-a-colour-from-a-palette]. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#picking-a-colour-from-a-palette https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#picking-a-colour-from-a-palette]. Blockly.Msg.COLOUR_PICKER_TOOLTIP = 'Choose a colour from the palette.'; /** @type {string} */ /// {{Optional}} url - A link that displays a random colour each time you visit it. @@ -195,25 +198,25 @@ Blockly.Msg.COLOUR_RANDOM_HELPURL = 'http://randomcolour.com'; /// block text - Title of block that generates a colour at random. Blockly.Msg.COLOUR_RANDOM_TITLE = 'random colour'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Colour#generating-a-random-colour https://github.com/google/blockly/wiki/Colour#generating-a-random-colour]. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#generating-a-random-colour https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#generating-a-random-colour]. Blockly.Msg.COLOUR_RANDOM_TOOLTIP = 'Choose a colour at random.'; /** @type {string} */ /// {{Optional}} url - A link for colour codes with percentages (0-100%) for each component, instead of the more common 0-255, which may be more difficult for beginners. Blockly.Msg.COLOUR_RGB_HELPURL = 'https://www.december.com/html/spec/colorpercompact.html'; /** @type {string} */ -/// block text - Title of block for [https://github.com/google/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components https://github.com/google/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components]. +/// block text - Title of block for [https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components]. Blockly.Msg.COLOUR_RGB_TITLE = 'colour with'; /** @type {string} */ -/// block input text - The amount of red (from 0 to 100) to use when [https://github.com/google/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components https://github.com/google/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components].\n{{Identical|Red}} +/// block input text - The amount of red (from 0 to 100) to use when [https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components].\n{{Identical|Red}} Blockly.Msg.COLOUR_RGB_RED = 'red'; /** @type {string} */ -/// block input text - The amount of green (from 0 to 100) to use when [https://github.com/google/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components https://github.com/google/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components]. +/// block input text - The amount of green (from 0 to 100) to use when [https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components]. Blockly.Msg.COLOUR_RGB_GREEN = 'green'; /** @type {string} */ -/// block input text - The amount of blue (from 0 to 100) to use when [https://github.com/google/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components https://github.com/google/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components].\n{{Identical|Blue}} +/// block input text - The amount of blue (from 0 to 100) to use when [https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components].\n{{Identical|Blue}} Blockly.Msg.COLOUR_RGB_BLUE = 'blue'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components https://github.com/google/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components]. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#creating-a-colour-from-red-green-and-blue-components]. Blockly.Msg.COLOUR_RGB_TOOLTIP = 'Create a colour with the specified amount of red, green, and blue. All values must be between 0 and 100.'; /** @type {string} */ /// {{Optional}} url - A useful link that displays blending of two colours. @@ -222,16 +225,16 @@ Blockly.Msg.COLOUR_BLEND_HELPURL = 'https://meyerweb.com/eric/tools/color-blend/ /// block text - A verb for blending two shades of paint. Blockly.Msg.COLOUR_BLEND_TITLE = 'blend'; /** @type {string} */ -/// block input text - The first of two colours to [https://github.com/google/blockly/wiki/Colour#blending-colours blend]. +/// block input text - The first of two colours to [https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#blending-colours blend]. Blockly.Msg.COLOUR_BLEND_COLOUR1 = 'colour 1'; /** @type {string} */ -/// block input text - The second of two colours to [https://github.com/google/blockly/wiki/Colour#blending-colours blend]. +/// block input text - The second of two colours to [https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#blending-colours blend]. Blockly.Msg.COLOUR_BLEND_COLOUR2 = 'colour 2'; /** @type {string} */ -/// block input text - The proportion of the [https://github.com/google/blockly/wiki/Colour#blending-colours blend] containing the first colour; the remaining proportion is of the second colour. For example, if the first colour is red and the second colour blue, a ratio of 1 would yield pure red, a ratio of .5 would yield purple (equal amounts of red and blue), and a ratio of 0 would yield pure blue.\n{{Identical|Ratio}} +/// block input text - The proportion of the [https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#blending-colours blend] containing the first colour; the remaining proportion is of the second colour. For example, if the first colour is red and the second colour blue, a ratio of 1 would yield pure red, a ratio of .5 would yield purple (equal amounts of red and blue), and a ratio of 0 would yield pure blue.\n{{Identical|Ratio}} Blockly.Msg.COLOUR_BLEND_RATIO = 'ratio'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Colour#blending-colours https://github.com/google/blockly/wiki/Colour#blending-colours]. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#blending-colours https://github.com/RaspberryPiFoundation/blockly/wiki/Colour#blending-colours]. Blockly.Msg.COLOUR_BLEND_TOOLTIP = 'Blends two colours together with a given ratio (0.0 - 1.0).'; // Loop Blocks. @@ -239,45 +242,45 @@ Blockly.Msg.COLOUR_BLEND_TOOLTIP = 'Blends two colours together with a given rat /// {{Optional}} url - Describes 'repeat loops' in computer programs; consider using the translation of the page [https://en.wikipedia.org/wiki/Control_flow https://en.wikipedia.org/wiki/Control_flow]. Blockly.Msg.CONTROLS_REPEAT_HELPURL = 'https://en.wikipedia.org/wiki/For_loop'; /** @type {string} */ -/// block input text - Title of [https://github.com/google/blockly/wiki/Loops#repeat repeat block].\n\nParameters:\n* %1 - the number of times the body of the loop should be repeated. +/// block input text - Title of [https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#repeat repeat block].\n\nParameters:\n* %1 - the number of times the body of the loop should be repeated. Blockly.Msg.CONTROLS_REPEAT_TITLE = 'repeat %1 times'; /** @type {string} */ -/// block text - Preceding the blocks in the body of the loop. See [https://github.com/google/blockly/wiki/Loops https://github.com/google/blockly/wiki/Loops].\n{{Identical|Do}} +/// block text - Preceding the blocks in the body of the loop. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Loops https://github.com/RaspberryPiFoundation/blockly/wiki/Loops].\n{{Identical|Do}} Blockly.Msg.CONTROLS_REPEAT_INPUT_DO = 'do'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Loops#repeat https://github.com/google/blockly/wiki/Loops#repeat]. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#repeat https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#repeat]. Blockly.Msg.CONTROLS_REPEAT_TOOLTIP = 'Do some statements several times.'; /** @type {string} */ /// {{Optional}} url - Describes 'while loops' in computer programs; consider using the translation of [https://en.wikipedia.org/wiki/While_loop https://en.wikipedia.org/wiki/While_loop], if present, or [https://en.wikipedia.org/wiki/Control_flow https://en.wikipedia.org/wiki/Control_flow]. -Blockly.Msg.CONTROLS_WHILEUNTIL_HELPURL = 'https://github.com/google/blockly/wiki/Loops#repeat'; +Blockly.Msg.CONTROLS_WHILEUNTIL_HELPURL = 'https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#repeat'; /** @type {string} */ Blockly.Msg.CONTROLS_WHILEUNTIL_INPUT_DO = Blockly.Msg.CONTROLS_REPEAT_INPUT_DO; /** @type {string} */ -/// dropdown - Specifies that a loop should [https://github.com/google/blockly/wiki/Loops#repeat-while repeat while] the following condition is true. +/// dropdown - Specifies that a loop should [https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#repeat-while repeat while] the following condition is true. Blockly.Msg.CONTROLS_WHILEUNTIL_OPERATOR_WHILE = 'repeat while'; /** @type {string} */ -/// dropdown - Specifies that a loop should [https://github.com/google/blockly/wiki/Loops#repeat-until repeat until] the following condition becomes true. +/// dropdown - Specifies that a loop should [https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#repeat-until repeat until] the following condition becomes true. Blockly.Msg.CONTROLS_WHILEUNTIL_OPERATOR_UNTIL = 'repeat until'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Loops#repeat-while Loops#repeat-while https://github.com/google/blockly/wiki/Loops#repeat-while Loops#repeat-while]. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#repeat-while Loops#repeat-while https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#repeat-while Loops#repeat-while]. Blockly.Msg.CONTROLS_WHILEUNTIL_TOOLTIP_WHILE = 'While a value is true, then do some statements.'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Loops#repeat-until https://github.com/google/blockly/wiki/Loops#repeat-until]. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#repeat-until https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#repeat-until]. Blockly.Msg.CONTROLS_WHILEUNTIL_TOOLTIP_UNTIL = 'While a value is false, then do some statements.'; /** @type {string} */ /// {{Optional}} url - Describes 'for loops' in computer programs. Consider using your language's translation of [https://en.wikipedia.org/wiki/For_loop https://en.wikipedia.org/wiki/For_loop], if present. -Blockly.Msg.CONTROLS_FOR_HELPURL = 'https://github.com/google/blockly/wiki/Loops#count-with'; +Blockly.Msg.CONTROLS_FOR_HELPURL = 'https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#count-with'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Loops#count-with https://github.com/google/blockly/wiki/Loops#count-with].\n\nParameters:\n* %1 - the name of the loop variable. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#count-with https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#count-with].\n\nParameters:\n* %1 - the name of the loop variable. Blockly.Msg.CONTROLS_FOR_TOOLTIP = 'Have the variable "%1" take on the values from the start number to the end number, counting by the specified interval, and do the specified blocks.'; /** @type {string} */ /// block text - Repeatedly counts a variable (%1) /// starting with a (usually lower) number in a range (%2), /// ending with a (usually higher) number in a range (%3), and counting the /// iterations by a number of steps (%4). As in -/// [https://github.com/google/blockly/wiki/Loops#count-with -/// https://github.com/google/blockly/wiki/Loops#count-with]. +/// [https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#count-with +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#count-with]. /// [[File:Blockly-count-with.png]] Blockly.Msg.CONTROLS_FOR_TITLE = 'count with %1 from %2 to %3 by %4'; /** @type {string} */ @@ -285,78 +288,78 @@ Blockly.Msg.CONTROLS_FOR_INPUT_DO = Blockly.Msg.CONTROLS_REPEAT_INPUT_DO; /** @type {string} */ /// {{Optional}} url - Describes 'for-each loops' in computer programs. Consider using your language's translation of [https://en.wikipedia.org/wiki/Foreach https://en.wikipedia.org/wiki/Foreach] if present. -Blockly.Msg.CONTROLS_FOREACH_HELPURL = 'https://github.com/google/blockly/wiki/Loops#for-each'; +Blockly.Msg.CONTROLS_FOREACH_HELPURL = 'https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#for-each'; /** @type {string} */ -/// block text - Title of [https://github.com/google/blockly/wiki/Loops#for-each for each block]. +/// block text - Title of [https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#for-each for each block]. /// Sequentially assigns every item in array %2 to the valiable %1. Blockly.Msg.CONTROLS_FOREACH_TITLE = 'for each item %1 in list %2'; /** @type {string} */ Blockly.Msg.CONTROLS_FOREACH_INPUT_DO = Blockly.Msg.CONTROLS_REPEAT_INPUT_DO; /** @type {string} */ -/// block text - Description of [https://github.com/google/blockly/wiki/Loops#for-each for each blocks].\n\nParameters:\n* %1 - the name of the loop variable. +/// block text - Description of [https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#for-each for each blocks].\n\nParameters:\n* %1 - the name of the loop variable. Blockly.Msg.CONTROLS_FOREACH_TOOLTIP = 'For each item in a list, set the variable "%1" to the item, and then do some statements.'; /** @type {string} */ /// {{Optional}} url - Describes control flow in computer programs. Consider using your language's translation of [https://en.wikipedia.org/wiki/Control_flow https://en.wikipedia.org/wiki/Control_flow], if it exists. -Blockly.Msg.CONTROLS_FLOW_STATEMENTS_HELPURL = 'https://github.com/google/blockly/wiki/Loops#loop-termination-blocks'; +Blockly.Msg.CONTROLS_FLOW_STATEMENTS_HELPURL = 'https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#loop-termination-blocks'; /** @type {string} */ -/// dropdown - The current loop should be exited. See [https://github.com/google/blockly/wiki/Loops#break https://github.com/google/blockly/wiki/Loops#break]. +/// dropdown - The current loop should be exited. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#break https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#break]. Blockly.Msg.CONTROLS_FLOW_STATEMENTS_OPERATOR_BREAK = 'break out of loop'; /** @type {string} */ -/// dropdown - The current iteration of the loop should be ended and the next should begin. See [https://github.com/google/blockly/wiki/Loops#continue-with-next-iteration https://github.com/google/blockly/wiki/Loops#continue-with-next-iteration]. +/// dropdown - The current iteration of the loop should be ended and the next should begin. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#continue-with-next-iteration https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#continue-with-next-iteration]. Blockly.Msg.CONTROLS_FLOW_STATEMENTS_OPERATOR_CONTINUE = 'continue with next iteration of loop'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Loops#break-out-of-loop https://github.com/google/blockly/wiki/Loops#break-out-of-loop]. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#break-out-of-loop https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#break-out-of-loop]. Blockly.Msg.CONTROLS_FLOW_STATEMENTS_TOOLTIP_BREAK = 'Break out of the containing loop.'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Loops#continue-with-next-iteration https://github.com/google/blockly/wiki/Loops#continue-with-next-iteration]. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#continue-with-next-iteration https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#continue-with-next-iteration]. Blockly.Msg.CONTROLS_FLOW_STATEMENTS_TOOLTIP_CONTINUE = 'Skip the rest of this loop, and continue with the next iteration.'; /** @type {string} */ -/// warning - The user has tried placing a block outside of a loop (for each, while, repeat, etc.), but this type of block may only be used within a loop. See [https://github.com/google/blockly/wiki/Loops#loop-termination-blocks https://github.com/google/blockly/wiki/Loops#loop-termination-blocks]. +/// warning - The user has tried placing a block outside of a loop (for each, while, repeat, etc.), but this type of block may only be used within a loop. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#loop-termination-blocks https://github.com/RaspberryPiFoundation/blockly/wiki/Loops#loop-termination-blocks]. Blockly.Msg.CONTROLS_FLOW_STATEMENTS_WARNING = 'Warning: This block may only be used within a loop.'; // Logic Blocks. /** @type {string} */ /// {{Optional}} url - Describes conditional statements (if-then-else) in computer programs. Consider using your language's translation of [https://en.wikipedia.org/wiki/If_else https://en.wikipedia.org/wiki/If_else], if present. -Blockly.Msg.CONTROLS_IF_HELPURL = 'https://github.com/google/blockly/wiki/IfElse'; +Blockly.Msg.CONTROLS_IF_HELPURL = 'https://github.com/RaspberryPiFoundation/blockly/wiki/IfElse'; /** @type {string} */ -/// tooltip - Describes [https://github.com/google/blockly/wiki/IfElse#if-blocks 'if' blocks]. Consider using your language's translation of [https://en.wikipedia.org/wiki/If_statement https://en.wikipedia.org/wiki/If_statement], if present. +/// tooltip - Describes [https://github.com/RaspberryPiFoundation/blockly/wiki/IfElse#if-blocks 'if' blocks]. Consider using your language's translation of [https://en.wikipedia.org/wiki/If_statement https://en.wikipedia.org/wiki/If_statement], if present. Blockly.Msg.CONTROLS_IF_TOOLTIP_1 = 'If a value is true, then do some statements.'; /** @type {string} */ -/// tooltip - Describes [https://github.com/google/blockly/wiki/IfElse#if-else-blocks if-else blocks]. Consider using your language's translation of [https://en.wikipedia.org/wiki/If_statement https://en.wikipedia.org/wiki/If_statement], if present. +/// tooltip - Describes [https://github.com/RaspberryPiFoundation/blockly/wiki/IfElse#if-else-blocks if-else blocks]. Consider using your language's translation of [https://en.wikipedia.org/wiki/If_statement https://en.wikipedia.org/wiki/If_statement], if present. Blockly.Msg.CONTROLS_IF_TOOLTIP_2 = 'If a value is true, then do the first block of statements. Otherwise, do the second block of statements.'; /** @type {string} */ -/// tooltip - Describes [https://github.com/google/blockly/wiki/IfElse#if-else-if-blocks if-else-if blocks]. Consider using your language's translation of [https://en.wikipedia.org/wiki/If_statement https://en.wikipedia.org/wiki/If_statement], if present. +/// tooltip - Describes [https://github.com/RaspberryPiFoundation/blockly/wiki/IfElse#if-else-if-blocks if-else-if blocks]. Consider using your language's translation of [https://en.wikipedia.org/wiki/If_statement https://en.wikipedia.org/wiki/If_statement], if present. Blockly.Msg.CONTROLS_IF_TOOLTIP_3 = 'If the first value is true, then do the first block of statements. Otherwise, if the second value is true, do the second block of statements.'; /** @type {string} */ -/// tooltip - Describes [https://github.com/google/blockly/wiki/IfElse#if-else-if-else-blocks if-else-if-else blocks]. Consider using your language's translation of [https://en.wikipedia.org/wiki/If_statement https://en.wikipedia.org/wiki/If_statement], if present. +/// tooltip - Describes [https://github.com/RaspberryPiFoundation/blockly/wiki/IfElse#if-else-if-else-blocks if-else-if-else blocks]. Consider using your language's translation of [https://en.wikipedia.org/wiki/If_statement https://en.wikipedia.org/wiki/If_statement], if present. Blockly.Msg.CONTROLS_IF_TOOLTIP_4 = 'If the first value is true, then do the first block of statements. Otherwise, if the second value is true, do the second block of statements. If none of the values are true, do the last block of statements.'; /** @type {string} */ -/// block text - See [https://github.com/google/blockly/wiki/IfElse https://github.com/google/blockly/wiki/IfElse]. +/// block text - See [https://github.com/RaspberryPiFoundation/blockly/wiki/IfElse https://github.com/RaspberryPiFoundation/blockly/wiki/IfElse]. /// It is recommended, but not essential, that this have text in common with the translation of 'else if'\n{{Identical|If}} Blockly.Msg.CONTROLS_IF_MSG_IF = 'if'; /** @type {string} */ -/// block text - See [https://github.com/google/blockly/wiki/IfElse https://github.com/google/blockly/wiki/IfElse]. The English words "otherwise if" would probably be clearer than "else if", but the latter is used because it is traditional and shorter. +/// block text - See [https://github.com/RaspberryPiFoundation/blockly/wiki/IfElse https://github.com/RaspberryPiFoundation/blockly/wiki/IfElse]. The English words "otherwise if" would probably be clearer than "else if", but the latter is used because it is traditional and shorter. Blockly.Msg.CONTROLS_IF_MSG_ELSEIF = 'else if'; /** @type {string} */ -/// block text - See [https://github.com/google/blockly/wiki/IfElse https://github.com/google/blockly/wiki/IfElse]. The English word "otherwise" would probably be superior to "else", but the latter is used because it is traditional and shorter. +/// block text - See [https://github.com/RaspberryPiFoundation/blockly/wiki/IfElse https://github.com/RaspberryPiFoundation/blockly/wiki/IfElse]. The English word "otherwise" would probably be superior to "else", but the latter is used because it is traditional and shorter. Blockly.Msg.CONTROLS_IF_MSG_ELSE = 'else'; /** @type {string} */ Blockly.Msg.CONTROLS_IF_MSG_THEN = Blockly.Msg.CONTROLS_REPEAT_INPUT_DO; /** @type {string} */ Blockly.Msg.CONTROLS_IF_IF_TITLE_IF = Blockly.Msg.CONTROLS_IF_MSG_IF; /** @type {string} */ -/// tooltip - Describes [https://github.com/google/blockly/wiki/IfElse#block-modification if block modification]. +/// tooltip - Describes [https://github.com/RaspberryPiFoundation/blockly/wiki/IfElse#block-modification if block modification]. Blockly.Msg.CONTROLS_IF_IF_TOOLTIP = 'Add, remove, or reorder sections to reconfigure this if block.'; /** @type {string} */ Blockly.Msg.CONTROLS_IF_ELSEIF_TITLE_ELSEIF = Blockly.Msg.CONTROLS_IF_MSG_ELSEIF; /** @type {string} */ -/// tooltip - Describes the 'else if' subblock during [https://github.com/google/blockly/wiki/IfElse#block-modification if block modification]. +/// tooltip - Describes the 'else if' subblock during [https://github.com/RaspberryPiFoundation/blockly/wiki/IfElse#block-modification if block modification]. Blockly.Msg.CONTROLS_IF_ELSEIF_TOOLTIP = 'Add a condition to the if block.'; /** @type {string} */ Blockly.Msg.CONTROLS_IF_ELSE_TITLE_ELSE = Blockly.Msg.CONTROLS_IF_MSG_ELSE; /** @type {string} */ -/// tooltip - Describes the 'else' subblock during [https://github.com/google/blockly/wiki/IfElse#block-modification if block modification]. +/// tooltip - Describes the 'else' subblock during [https://github.com/RaspberryPiFoundation/blockly/wiki/IfElse#block-modification if block modification]. Blockly.Msg.CONTROLS_IF_ELSE_TOOLTIP = 'Add a final, catch-all condition to the if block.'; /** @type {string} */ @@ -383,7 +386,7 @@ Blockly.Msg.LOGIC_COMPARE_TOOLTIP_GTE = 'Return true if the first input is great /** @type {string} */ /// {{Optional}} url - Information about the Boolean conjunction ("and") and disjunction ("or") operators. Consider using the translation of [https://en.wikipedia.org/wiki/Boolean_logic https://en.wikipedia.org/wiki/Boolean_logic], if it exists in your language. -Blockly.Msg.LOGIC_OPERATION_HELPURL = 'https://github.com/google/blockly/wiki/Logic#logical-operations'; +Blockly.Msg.LOGIC_OPERATION_HELPURL = 'https://github.com/RaspberryPiFoundation/blockly/wiki/Logic#logical-operations'; /** @type {string} */ /// tooltip - See [https://en.wikipedia.org/wiki/Logical_conjunction https://en.wikipedia.org/wiki/Logical_conjunction]. Blockly.Msg.LOGIC_OPERATION_TOOLTIP_AND = 'Return true if both inputs are true.'; @@ -399,7 +402,7 @@ Blockly.Msg.LOGIC_OPERATION_OR = 'or'; /** @type {string} */ /// {{Optional}} url - Information about logical negation. The translation of [https://en.wikipedia.org/wiki/Logical_negation https://en.wikipedia.org/wiki/Logical_negation] is recommended if it exists in the target language. -Blockly.Msg.LOGIC_NEGATE_HELPURL = 'https://github.com/google/blockly/wiki/Logic#not'; +Blockly.Msg.LOGIC_NEGATE_HELPURL = 'https://github.com/RaspberryPiFoundation/blockly/wiki/Logic#not'; /** @type {string} */ /// block text - This is a unary operator that returns ''false'' when the input is ''true'', and ''true'' when the input is ''false''. /// \n\nParameters:\n* %1 - the input (which should be either the value "true" or "false") @@ -410,7 +413,7 @@ Blockly.Msg.LOGIC_NEGATE_TOOLTIP = 'Returns true if the input is false. Returns /** @type {string} */ /// {{Optional}} url - Information about the logic values ''true'' and ''false''. Consider using the translation of [https://en.wikipedia.org/wiki/Truth_value https://en.wikipedia.org/wiki/Truth_value] if it exists in your language. -Blockly.Msg.LOGIC_BOOLEAN_HELPURL = 'https://github.com/google/blockly/wiki/Logic#values'; +Blockly.Msg.LOGIC_BOOLEAN_HELPURL = 'https://github.com/RaspberryPiFoundation/blockly/wiki/Logic#values'; /** @type {string} */ /// block text - The word for the [https://en.wikipedia.org/wiki/Truth_value logical value] ''true''.\n{{Identical|True}} Blockly.Msg.LOGIC_BOOLEAN_TRUE = 'true'; @@ -745,34 +748,34 @@ Blockly.Msg.MATH_ATAN2_TOOLTIP = 'Return the arctangent of point (X, Y) in degre /// {{Optional}} url - Information about how computers represent text (sometimes referred to as ''string''s). Blockly.Msg.TEXT_TEXT_HELPURL = 'https://en.wikipedia.org/wiki/String_(computer_science)'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Text https://github.com/google/blockly/wiki/Text]. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text https://github.com/RaspberryPiFoundation/blockly/wiki/Text]. Blockly.Msg.TEXT_TEXT_TOOLTIP = 'A letter, word, or line of text.'; /** @type {string} */ /// {{Optional}} url - Information on concatenating/appending pieces of text. -Blockly.Msg.TEXT_JOIN_HELPURL = 'https://github.com/google/blockly/wiki/Text#text-creation'; +Blockly.Msg.TEXT_JOIN_HELPURL = 'https://github.com/RaspberryPiFoundation/blockly/wiki/Text#text-creation'; /** @type {string} */ -/// block text - See [https://github.com/google/blockly/wiki/Text#text-creation https://github.com/google/blockly/wiki/Text#text-creation]. +/// block text - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#text-creation https://github.com/RaspberryPiFoundation/blockly/wiki/Text#text-creation]. Blockly.Msg.TEXT_JOIN_TITLE_CREATEWITH = 'create text with'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Text#text-creation create text with] for more information. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#text-creation create text with] for more information. Blockly.Msg.TEXT_JOIN_TOOLTIP = 'Create a piece of text by joining together any number of items.'; /** @type {string} */ -/// block text - This is shown when the programmer wants to change the number of pieces of text being joined together. See [https://github.com/google/blockly/wiki/Text#text-creation https://github.com/google/blockly/wiki/Text#text-creation], specifically the last picture in the 'Text creation' section.\n{{Identical|Join}} +/// block text - This is shown when the programmer wants to change the number of pieces of text being joined together. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#text-creation https://github.com/RaspberryPiFoundation/blockly/wiki/Text#text-creation], specifically the last picture in the 'Text creation' section.\n{{Identical|Join}} Blockly.Msg.TEXT_CREATE_JOIN_TITLE_JOIN = 'join'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Text#text-creation https://github.com/google/blockly/wiki/Text#text-creation], specifically the last picture in the 'Text creation' section. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#text-creation https://github.com/RaspberryPiFoundation/blockly/wiki/Text#text-creation], specifically the last picture in the 'Text creation' section. Blockly.Msg.TEXT_CREATE_JOIN_TOOLTIP = 'Add, remove, or reorder sections to reconfigure this text block.'; /** @type {string} */ Blockly.Msg.TEXT_CREATE_JOIN_ITEM_TITLE_ITEM = Blockly.Msg.VARIABLES_DEFAULT_NAME; /** @type {string} */ -/// block text - See [https://github.com/google/blockly/wiki/Text#text-creation https://github.com/google/blockly/wiki/Text#text-creation], specifically the last picture in the 'Text creation' section. +/// block text - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#text-creation https://github.com/RaspberryPiFoundation/blockly/wiki/Text#text-creation], specifically the last picture in the 'Text creation' section. Blockly.Msg.TEXT_CREATE_JOIN_ITEM_TOOLTIP = 'Add an item to the text.'; /** @type {string} */ /// {{Optional}} url - This and the other text-related URLs are going to be hard to translate. As always, it is okay to leave untranslated or paste in the English-language URL. For these URLs, you might also consider a general URL about how computers represent text (such as the translation of [https://en.wikipedia.org/wiki/String_(computer_science) this Wikipedia page]). -Blockly.Msg.TEXT_APPEND_HELPURL = 'https://github.com/google/blockly/wiki/Text#text-modification'; +Blockly.Msg.TEXT_APPEND_HELPURL = 'https://github.com/RaspberryPiFoundation/blockly/wiki/Text#text-modification'; /** @type {string} */ /// block input text - Message that the variable name at %1 will have the item at %2 appended to it. /// [[File:blockly-append-text.png]] @@ -780,54 +783,54 @@ Blockly.Msg.TEXT_APPEND_TITLE = 'to %1 append text %2'; /** @type {string} */ Blockly.Msg.TEXT_APPEND_VARIABLE = Blockly.Msg.VARIABLES_DEFAULT_NAME; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Text#text-modification https://github.com/google/blockly/wiki/Text#text-modification] for more information.\n\nParameters:\n* %1 - the name of the variable to which text should be appended +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#text-modification https://github.com/RaspberryPiFoundation/blockly/wiki/Text#text-modification] for more information.\n\nParameters:\n* %1 - the name of the variable to which text should be appended Blockly.Msg.TEXT_APPEND_TOOLTIP = 'Append some text to variable "%1".'; /** @type {string} */ /// {{Optional}} url - Information about text on computers (usually referred to as 'strings'). -Blockly.Msg.TEXT_LENGTH_HELPURL = 'https://github.com/google/blockly/wiki/Text#text-modification'; +Blockly.Msg.TEXT_LENGTH_HELPURL = 'https://github.com/RaspberryPiFoundation/blockly/wiki/Text#text-modification'; /** @type {string} */ -/// block text - See [https://github.com/google/blockly/wiki/Text#text-length https://github.com/google/blockly/wiki/Text#text-length]. +/// block text - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#text-length https://github.com/RaspberryPiFoundation/blockly/wiki/Text#text-length]. /// \n\nParameters:\n* %1 - the piece of text to take the length of Blockly.Msg.TEXT_LENGTH_TITLE = 'length of %1'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Text#text-length https://github.com/google/blockly/wiki/Text#text-length]. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#text-length https://github.com/RaspberryPiFoundation/blockly/wiki/Text#text-length]. Blockly.Msg.TEXT_LENGTH_TOOLTIP = 'Returns the number of letters (including spaces) in the provided text.'; /** @type {string} */ /// {{Optional}} url - Information about empty pieces of text on computers (usually referred to as 'empty strings'). -Blockly.Msg.TEXT_ISEMPTY_HELPURL = 'https://github.com/google/blockly/wiki/Text#checking-for-empty-text'; +Blockly.Msg.TEXT_ISEMPTY_HELPURL = 'https://github.com/RaspberryPiFoundation/blockly/wiki/Text#checking-for-empty-text'; /** @type {string} */ -/// block text - See [https://github.com/google/blockly/wiki/Text#checking-for-empty-text https://github.com/google/blockly/wiki/Text#checking-for-empty-text]. +/// block text - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#checking-for-empty-text https://github.com/RaspberryPiFoundation/blockly/wiki/Text#checking-for-empty-text]. /// \n\nParameters:\n* %1 - the piece of text to test for emptiness Blockly.Msg.TEXT_ISEMPTY_TITLE = '%1 is empty'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Text#checking-for-empty-text https://github.com/google/blockly/wiki/Text#checking-for-empty-text]. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#checking-for-empty-text https://github.com/RaspberryPiFoundation/blockly/wiki/Text#checking-for-empty-text]. Blockly.Msg.TEXT_ISEMPTY_TOOLTIP = 'Returns true if the provided text is empty.'; /** @type {string} */ /// {{Optional}} url - Information about finding a character in a piece of text. -Blockly.Msg.TEXT_INDEXOF_HELPURL = 'https://github.com/google/blockly/wiki/Text#finding-text'; +Blockly.Msg.TEXT_INDEXOF_HELPURL = 'https://github.com/RaspberryPiFoundation/blockly/wiki/Text#finding-text'; /** @type {string} */ -/// tooltip - %1 will be replaced by either the number 0 or -1 depending on the indexing mode. See [https://github.com/google/blockly/wiki/Text#finding-text https://github.com/google/blockly/wiki/Text#finding-text]. +/// tooltip - %1 will be replaced by either the number 0 or -1 depending on the indexing mode. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#finding-text https://github.com/RaspberryPiFoundation/blockly/wiki/Text#finding-text]. Blockly.Msg.TEXT_INDEXOF_TOOLTIP = 'Returns the index of the first/last occurrence of the first text in the second text. Returns %1 if text is not found.'; /** @type {string} */ /// block text - Title of blocks allowing users to find text. See -/// [https://github.com/google/blockly/wiki/Text#finding-text -/// https://github.com/google/blockly/wiki/Text#finding-text]. +/// [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#finding-text +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Text#finding-text]. /// [[File:Blockly-find-text.png]]. /// In English the expanded message is "in text %1 find (first|last) occurance of text %3" /// where %1 and %3 are added by the user. See TEXT_INDEXOF_OPERATOR_FIRST and /// TEXT_INDEXOF_OPERATOR_LAST for the dropdown text that replaces %2. Blockly.Msg.TEXT_INDEXOF_TITLE = 'in text %1 %2 %3'; /** @type {string} */ -/// dropdown - See [https://github.com/google/blockly/wiki/Text#finding-text -/// https://github.com/google/blockly/wiki/Text#finding-text]. +/// dropdown - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#finding-text +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Text#finding-text]. /// [[File:Blockly-find-text.png]]. Blockly.Msg.TEXT_INDEXOF_OPERATOR_FIRST = 'find first occurrence of text'; /** @type {string} */ -/// dropdown - See [https://github.com/google/blockly/wiki/Text#finding-text -/// https://github.com/google/blockly/wiki/Text#finding-text]. This would +/// dropdown - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#finding-text +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Text#finding-text]. This would /// replace "find first occurrence of text" below. (For more information on /// how common text is factored out of dropdown menus, see /// [https://translatewiki.net/wiki/Translating:Blockly#Drop-Down_Menus @@ -837,48 +840,48 @@ Blockly.Msg.TEXT_INDEXOF_OPERATOR_LAST = 'find last occurrence of text'; /** @type {string} */ /// {{Optional}} url - Information about extracting characters (letters, number, symbols, etc.) from text. -Blockly.Msg.TEXT_CHARAT_HELPURL = 'https://github.com/google/blockly/wiki/Text#extracting-text'; +Blockly.Msg.TEXT_CHARAT_HELPURL = 'https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-text'; /** @type {string} */ /// block text - Text for a block to extract a letter (or number, /// punctuation character, etc.) from a string, as shown below. %1 is added by /// the user and %2 is replaced by a dropdown of options, possibly followed by /// another user supplied string. TEXT_CHARAT_TAIL is then added to the end. See -/// [https://github.com/google/blockly/wiki/Text#extracting-a-single-character -/// https://github.com/google/blockly/wiki/Text#extracting-a-single-character]. +/// [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-single-character +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-single-character]. /// [[File:Blockly-text-get.png]] Blockly.Msg.TEXT_CHARAT_TITLE = 'in text %1 %2'; /** @type {string} */ /// dropdown - Indicates that the letter (or number, punctuation character, etc.) with the /// specified index should be obtained from the preceding piece of text. See -/// [https://github.com/google/blockly/wiki/Text#extracting-a-single-character -/// https://github.com/google/blockly/wiki/Text#extracting-a-single-character]. +/// [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-single-character +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-single-character]. /// [[File:Blockly-text-get.png]] Blockly.Msg.TEXT_CHARAT_FROM_START = 'get letter #'; /** @type {string} */ /// block text - Indicates that the letter (or number, punctuation character, etc.) with the /// specified index from the end of a given piece of text should be obtained. See -/// [https://github.com/google/blockly/wiki/Text#extracting-a-single-character -/// https://github.com/google/blockly/wiki/Text#extracting-a-single-character]. +/// [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-single-character +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-single-character]. /// [[File:Blockly-text-get.png]] Blockly.Msg.TEXT_CHARAT_FROM_END = 'get letter # from end'; /** @type {string} */ /// block text - Indicates that the first letter of the following piece of text should be -/// retrieved. See [https://github.com/google/blockly/wiki/Text#extracting-a-single-character -/// https://github.com/google/blockly/wiki/Text#extracting-a-single-character]. +/// retrieved. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-single-character +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-single-character]. /// [[File:Blockly-text-get.png]] Blockly.Msg.TEXT_CHARAT_FIRST = 'get first letter'; /** @type {string} */ /// block text - Indicates that the last letter (or number, punctuation mark, etc.) of the /// following piece of text should be retrieved. See -/// [https://github.com/google/blockly/wiki/Text#extracting-a-single-character -/// https://github.com/google/blockly/wiki/Text#extracting-a-single-character]. +/// [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-single-character +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-single-character]. /// [[File:Blockly-text-get.png]] Blockly.Msg.TEXT_CHARAT_LAST = 'get last letter'; /** @type {string} */ /// block text - Indicates that any letter (or number, punctuation mark, etc.) in the /// following piece of text should be randomly selected. See -/// [https://github.com/google/blockly/wiki/Text#extracting-a-single-character -/// https://github.com/google/blockly/wiki/Text#extracting-a-single-character]. +/// [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-single-character +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-single-character]. /// [[File:Blockly-text-get.png]] Blockly.Msg.TEXT_CHARAT_RANDOM = 'get random letter'; /** @type {string} */ @@ -889,19 +892,19 @@ Blockly.Msg.TEXT_CHARAT_RANDOM = 'get random letter'; /// [[File:Blockly-text-get.png]] Blockly.Msg.TEXT_CHARAT_TAIL = ''; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Text#extracting-a-single-character -/// https://github.com/google/blockly/wiki/Text#extracting-a-single-character]. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-single-character +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-single-character]. /// [[File:Blockly-text-get.png]] Blockly.Msg.TEXT_CHARAT_TOOLTIP = 'Returns the letter at the specified position.'; /** @type {string} */ -/// See [https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text -/// https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text]. +/// See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-region-of-text +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-region-of-text]. Blockly.Msg.TEXT_GET_SUBSTRING_TOOLTIP = 'Returns a specified portion of the text.'; /** @type {string} */ /// {{Optional}} url - Information about extracting characters from text. Reminder: urls are the /// lowest priority translations. Feel free to skip. -Blockly.Msg.TEXT_GET_SUBSTRING_HELPURL = 'https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text'; +Blockly.Msg.TEXT_GET_SUBSTRING_HELPURL = 'https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-region-of-text'; /** @type {string} */ /// block text - Precedes a piece of text from which a portion should be extracted. /// [[File:Blockly-get-substring.png]] @@ -909,15 +912,15 @@ Blockly.Msg.TEXT_GET_SUBSTRING_INPUT_IN_TEXT = 'in text'; /** @type {string} */ /// dropdown - Indicates that the following number specifies the position (relative to the start /// position) of the beginning of the region of text that should be obtained from the preceding -/// piece of text. See [https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text -/// https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text]. +/// piece of text. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-region-of-text +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-region-of-text]. /// [[File:Blockly-get-substring.png]] Blockly.Msg.TEXT_GET_SUBSTRING_START_FROM_START = 'get substring from letter #'; /** @type {string} */ /// dropdown - Indicates that the following number specifies the position (relative to the end /// position) of the beginning of the region of text that should be obtained from the preceding -/// piece of text. See [https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text -/// https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text]. +/// piece of text. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-region-of-text +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-region-of-text]. /// Note: If {{msg-blockly|ORDINAL_NUMBER_SUFFIX}} is defined, it will /// automatically appear ''after'' this and any other /// [https://translatewiki.net/wiki/Translating:Blockly#Ordinal_numbers ordinal numbers] @@ -927,125 +930,125 @@ Blockly.Msg.TEXT_GET_SUBSTRING_START_FROM_END = 'get substring from letter # fro /** @type {string} */ /// block text - Indicates that a region starting with the first letter of the preceding piece /// of text should be extracted. See -/// [https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text -/// https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text]. +/// [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-region-of-text +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-region-of-text]. /// [[File:Blockly-get-substring.png]] Blockly.Msg.TEXT_GET_SUBSTRING_START_FIRST = 'get substring from first letter'; /** @type {string} */ /// dropdown - Indicates that the following number specifies the position (relative to /// the start position) of the end of the region of text that should be obtained from the /// preceding piece of text. See -/// [https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text -/// https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text]. +/// [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-region-of-text +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-region-of-text]. /// [[File:Blockly-get-substring.png]] Blockly.Msg.TEXT_GET_SUBSTRING_END_FROM_START = 'to letter #'; /** @type {string} */ /// dropdown - Indicates that the following number specifies the position (relative to the /// end position) of the end of the region of text that should be obtained from the preceding /// piece of text. See -/// [https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text -/// https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text]. +/// [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-region-of-text +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-region-of-text]. /// [[File:Blockly-get-substring.png]] Blockly.Msg.TEXT_GET_SUBSTRING_END_FROM_END = 'to letter # from end'; /** @type {string} */ /// block text - Indicates that a region ending with the last letter of the preceding piece /// of text should be extracted. See -/// [https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text -/// https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text]. +/// [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-region-of-text +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-region-of-text]. /// [[File:Blockly-get-substring.png]] Blockly.Msg.TEXT_GET_SUBSTRING_END_LAST = 'to last letter'; /** @type {string} */ /// {{Optional|Supply translation only if your language requires it. Most do not.}} /// block text - Text that should go after the rightmost block/dropdown when -/// [https://github.com/google/blockly/wiki/Text#extracting-a-region-of-text +/// [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#extracting-a-region-of-text /// extracting a region of text]. In most languages, this will be the empty string. /// [[File:Blockly-get-substring.png]] Blockly.Msg.TEXT_GET_SUBSTRING_TAIL = ''; /** @type {string} */ /// {{Optional}} url - Information about the case of letters (upper-case and lower-case). -Blockly.Msg.TEXT_CHANGECASE_HELPURL = 'https://github.com/google/blockly/wiki/Text#adjusting-text-case'; +Blockly.Msg.TEXT_CHANGECASE_HELPURL = 'https://github.com/RaspberryPiFoundation/blockly/wiki/Text#adjusting-text-case'; /** @type {string} */ /// tooltip - Describes a block to adjust the case of letters. For more information on this block, -/// see [https://github.com/google/blockly/wiki/Text#adjusting-text-case -/// https://github.com/google/blockly/wiki/Text#adjusting-text-case]. +/// see [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#adjusting-text-case +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Text#adjusting-text-case]. Blockly.Msg.TEXT_CHANGECASE_TOOLTIP = 'Return a copy of the text in a different case.'; /** @type {string} */ /// block text - Indicates that all of the letters in the following piece of text should be /// capitalized. If your language does not use case, you may indicate that this is not /// applicable to your language. For more information on this block, see -/// [https://github.com/google/blockly/wiki/Text#adjusting-text-case -/// https://github.com/google/blockly/wiki/Text#adjusting-text-case]. +/// [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#adjusting-text-case +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Text#adjusting-text-case]. Blockly.Msg.TEXT_CHANGECASE_OPERATOR_UPPERCASE = 'to UPPER CASE'; /** @type {string} */ -/// block text - Indicates that all of the letters in the following piece of text should be converted to lower-case. If your language does not use case, you may indicate that this is not applicable to your language. For more information on this block, see [https://github.com/google/blockly/wiki/Text#adjusting-text-case https://github.com/google/blockly/wiki/Text#adjusting-text-case]. +/// block text - Indicates that all of the letters in the following piece of text should be converted to lower-case. If your language does not use case, you may indicate that this is not applicable to your language. For more information on this block, see [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#adjusting-text-case https://github.com/RaspberryPiFoundation/blockly/wiki/Text#adjusting-text-case]. Blockly.Msg.TEXT_CHANGECASE_OPERATOR_LOWERCASE = 'to lower case'; /** @type {string} */ -/// block text - Indicates that the first letter of each of the following words should be capitalized and the rest converted to lower-case. If your language does not use case, you may indicate that this is not applicable to your language. For more information on this block, see [https://github.com/google/blockly/wiki/Text#adjusting-text-case https://github.com/google/blockly/wiki/Text#adjusting-text-case]. +/// block text - Indicates that the first letter of each of the following words should be capitalized and the rest converted to lower-case. If your language does not use case, you may indicate that this is not applicable to your language. For more information on this block, see [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#adjusting-text-case https://github.com/RaspberryPiFoundation/blockly/wiki/Text#adjusting-text-case]. Blockly.Msg.TEXT_CHANGECASE_OPERATOR_TITLECASE = 'to Title Case'; /** @type {string} */ /// {{Optional}} url - Information about trimming (removing) text off the beginning and ends of pieces of text. -Blockly.Msg.TEXT_TRIM_HELPURL = 'https://github.com/google/blockly/wiki/Text#trimming-removing-spaces'; +Blockly.Msg.TEXT_TRIM_HELPURL = 'https://github.com/RaspberryPiFoundation/blockly/wiki/Text#trimming-removing-spaces'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Text#trimming-removing-spaces -/// https://github.com/google/blockly/wiki/Text#trimming-removing-spaces]. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#trimming-removing-spaces +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Text#trimming-removing-spaces]. Blockly.Msg.TEXT_TRIM_TOOLTIP = 'Return a copy of the text with spaces removed from one or both ends.'; /** @type {string} */ /// dropdown - Removes spaces from the beginning and end of a piece of text. See -/// [https://github.com/google/blockly/wiki/Text#trimming-removing-spaces -/// https://github.com/google/blockly/wiki/Text#trimming-removing-spaces]. Note that neither +/// [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#trimming-removing-spaces +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Text#trimming-removing-spaces]. Note that neither /// this nor the other options modify the original piece of text (that follows); /// the block just returns a version of the text without the specified spaces. Blockly.Msg.TEXT_TRIM_OPERATOR_BOTH = 'trim spaces from both sides of'; /** @type {string} */ /// dropdown - Removes spaces from the beginning of a piece of text. See -/// [https://github.com/google/blockly/wiki/Text#trimming-removing-spaces -/// https://github.com/google/blockly/wiki/Text#trimming-removing-spaces]. +/// [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#trimming-removing-spaces +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Text#trimming-removing-spaces]. /// Note that in right-to-left scripts, this will remove spaces from the right side. Blockly.Msg.TEXT_TRIM_OPERATOR_LEFT = 'trim spaces from left side of'; /** @type {string} */ /// dropdown - Removes spaces from the end of a piece of text. See -/// [https://github.com/google/blockly/wiki/Text#trimming-removing-spaces -/// https://github.com/google/blockly/wiki/Text#trimming-removing-spaces]. +/// [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#trimming-removing-spaces +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Text#trimming-removing-spaces]. /// Note that in right-to-left scripts, this will remove spaces from the left side. Blockly.Msg.TEXT_TRIM_OPERATOR_RIGHT = 'trim spaces from right side of'; /** @type {string} */ /// {{Optional}} url - Information about displaying text on computers. -Blockly.Msg.TEXT_PRINT_HELPURL = 'https://github.com/google/blockly/wiki/Text#printing-text'; +Blockly.Msg.TEXT_PRINT_HELPURL = 'https://github.com/RaspberryPiFoundation/blockly/wiki/Text#printing-text'; /** @type {string} */ /// block text - Display the input on the screen. See -/// [https://github.com/google/blockly/wiki/Text#printing-text -/// https://github.com/google/blockly/wiki/Text#printing-text]. +/// [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#printing-text +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Text#printing-text]. /// \n\nParameters:\n* %1 - the value to print Blockly.Msg.TEXT_PRINT_TITLE = 'print %1'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Text#printing-text -/// https://github.com/google/blockly/wiki/Text#printing-text]. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#printing-text +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Text#printing-text]. Blockly.Msg.TEXT_PRINT_TOOLTIP = 'Print the specified text, number or other value.'; /** @type {string} */ /// {{Optional}} url - Information about getting text from users. -Blockly.Msg.TEXT_PROMPT_HELPURL = 'https://github.com/google/blockly/wiki/Text#getting-input-from-the-user'; +Blockly.Msg.TEXT_PROMPT_HELPURL = 'https://github.com/RaspberryPiFoundation/blockly/wiki/Text#getting-input-from-the-user'; /** @type {string} */ /// dropdown - Specifies that a piece of text should be requested from the user with -/// the following message. See [https://github.com/google/blockly/wiki/Text#printing-text -/// https://github.com/google/blockly/wiki/Text#printing-text]. +/// the following message. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#printing-text +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Text#printing-text]. Blockly.Msg.TEXT_PROMPT_TYPE_TEXT = 'prompt for text with message'; /** @type {string} */ /// dropdown - Specifies that a number should be requested from the user with the -/// following message. See [https://github.com/google/blockly/wiki/Text#printing-text -/// https://github.com/google/blockly/wiki/Text#printing-text]. +/// following message. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#printing-text +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Text#printing-text]. Blockly.Msg.TEXT_PROMPT_TYPE_NUMBER = 'prompt for number with message'; /** @type {string} */ /// dropdown - Precedes the message with which the user should be prompted for -/// a number. See [https://github.com/google/blockly/wiki/Text#printing-text -/// https://github.com/google/blockly/wiki/Text#printing-text]. +/// a number. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#printing-text +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Text#printing-text]. Blockly.Msg.TEXT_PROMPT_TOOLTIP_NUMBER = 'Prompt for user for a number.'; /** @type {string} */ /// dropdown - Precedes the message with which the user should be prompted for some text. -/// See [https://github.com/google/blockly/wiki/Text#printing-text -/// https://github.com/google/blockly/wiki/Text#printing-text]. +/// See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text#printing-text +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Text#printing-text]. Blockly.Msg.TEXT_PROMPT_TOOLTIP_TEXT = 'Prompt for user for some text.'; /** @type {string} */ @@ -1054,7 +1057,7 @@ Blockly.Msg.TEXT_PROMPT_TOOLTIP_TEXT = 'Prompt for user for some text.'; Blockly.Msg.TEXT_COUNT_MESSAGE0 = 'count %1 in %2'; /** @type {string} */ /// {{Optional}} url - Information about counting how many times a string appears in another string. -Blockly.Msg.TEXT_COUNT_HELPURL = 'https://github.com/google/blockly/wiki/Text#counting-substrings'; +Blockly.Msg.TEXT_COUNT_HELPURL = 'https://github.com/RaspberryPiFoundation/blockly/wiki/Text#counting-substrings'; /** @type {string} */ /// tooltip - Short description of a block that counts how many times some text occurs within some other text. Blockly.Msg.TEXT_COUNT_TOOLTIP = 'Count how many times some text occurs within some other text.'; @@ -1065,7 +1068,7 @@ Blockly.Msg.TEXT_COUNT_TOOLTIP = 'Count how many times some text occurs within s Blockly.Msg.TEXT_REPLACE_MESSAGE0 = 'replace %1 with %2 in %3'; /** @type {string} */ /// {{Optional}} url - Information about replacing each copy text (or string, in computer lingo) with other text. -Blockly.Msg.TEXT_REPLACE_HELPURL = 'https://github.com/google/blockly/wiki/Text#replacing-substrings'; +Blockly.Msg.TEXT_REPLACE_HELPURL = 'https://github.com/RaspberryPiFoundation/blockly/wiki/Text#replacing-substrings'; /** @type {string} */ /// tooltip - Short description of a block that replaces copies of text in a large text with other text. Blockly.Msg.TEXT_REPLACE_TOOLTIP = 'Replace all occurances of some text within some other text.'; @@ -1076,128 +1079,128 @@ Blockly.Msg.TEXT_REPLACE_TOOLTIP = 'Replace all occurances of some text within s Blockly.Msg.TEXT_REVERSE_MESSAGE0 = 'reverse %1'; /** @type {string} */ /// {{Optional}} url - Information about reversing a letters/characters in text. -Blockly.Msg.TEXT_REVERSE_HELPURL = 'https://github.com/google/blockly/wiki/Text#reversing-text'; +Blockly.Msg.TEXT_REVERSE_HELPURL = 'https://github.com/RaspberryPiFoundation/blockly/wiki/Text#reversing-text'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Text]. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Text]. Blockly.Msg.TEXT_REVERSE_TOOLTIP = 'Reverses the order of the characters in the text.'; // Lists Blocks. /** @type {string} */ /// {{Optional}} url - Information on empty lists. -Blockly.Msg.LISTS_CREATE_EMPTY_HELPURL = 'https://github.com/google/blockly/wiki/Lists#create-empty-list'; +Blockly.Msg.LISTS_CREATE_EMPTY_HELPURL = 'https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#create-empty-list'; /** @type {string} */ -/// block text - See [https://github.com/google/blockly/wiki/Lists#create-empty-list https://github.com/google/blockly/wiki/Lists#create-empty-list]. +/// block text - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#create-empty-list https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#create-empty-list]. Blockly.Msg.LISTS_CREATE_EMPTY_TITLE = 'create empty list'; /** @type {string} */ -/// block text - See [https://github.com/google/blockly/wiki/Lists#create-empty-list https://github.com/google/blockly/wiki/Lists#create-empty-list]. +/// block text - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#create-empty-list https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#create-empty-list]. Blockly.Msg.LISTS_CREATE_EMPTY_TOOLTIP = 'Returns a list, of length 0, containing no data records'; /** @type {string} */ /// {{Optional}} url - Information on building lists. -Blockly.Msg.LISTS_CREATE_WITH_HELPURL = 'https://github.com/google/blockly/wiki/Lists#create-list-with'; +Blockly.Msg.LISTS_CREATE_WITH_HELPURL = 'https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#create-list-with'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Lists#create-list-with https://github.com/google/blockly/wiki/Lists#create-list-with]. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#create-list-with https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#create-list-with]. Blockly.Msg.LISTS_CREATE_WITH_TOOLTIP = 'Create a list with any number of items.'; /** @type {string} */ -/// block text - See [https://github.com/google/blockly/wiki/Lists#create-list-with https://github.com/google/blockly/wiki/Lists#create-list-with]. +/// block text - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#create-list-with https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#create-list-with]. Blockly.Msg.LISTS_CREATE_WITH_INPUT_WITH = 'create list with'; /** @type {string} */ -/// block text - This appears in a sub-block when [https://github.com/google/blockly/wiki/Lists#changing-number-of-inputs changing the number of inputs in a ''''create list with'''' block].\n{{Identical|List}} +/// block text - This appears in a sub-block when [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#changing-number-of-inputs changing the number of inputs in a ''''create list with'''' block].\n{{Identical|List}} Blockly.Msg.LISTS_CREATE_WITH_CONTAINER_TITLE_ADD = 'list'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Lists#changing-number-of-inputs https://github.com/google/blockly/wiki/Lists#changing-number-of-inputs]. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#changing-number-of-inputs https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#changing-number-of-inputs]. Blockly.Msg.LISTS_CREATE_WITH_CONTAINER_TOOLTIP = 'Add, remove, or reorder sections to reconfigure this list block.'; /** @type {string} */ Blockly.Msg.LISTS_CREATE_WITH_ITEM_TITLE = Blockly.Msg.VARIABLES_DEFAULT_NAME; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Lists#changing-number-of-inputs https://github.com/google/blockly/wiki/Lists#changing-number-of-inputs]. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#changing-number-of-inputs https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#changing-number-of-inputs]. Blockly.Msg.LISTS_CREATE_WITH_ITEM_TOOLTIP = 'Add an item to the list.'; /** @type {string} */ -/// {{Optional}} url - Information about [https://github.com/google/blockly/wiki/Lists#create-list-with creating a list with multiple copies of a single item]. -Blockly.Msg.LISTS_REPEAT_HELPURL = 'https://github.com/google/blockly/wiki/Lists#create-list-with'; +/// {{Optional}} url - Information about [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#create-list-with creating a list with multiple copies of a single item]. +Blockly.Msg.LISTS_REPEAT_HELPURL = 'https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#create-list-with'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Lists#create-list-with creating a list with multiple copies of a single item]. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#create-list-with creating a list with multiple copies of a single item]. Blockly.Msg.LISTS_REPEAT_TOOLTIP = 'Creates a list consisting of the given value repeated the specified number of times.'; /** @type {string} */ -/// block text - See [https://github.com/google/blockly/wiki/Lists#create-list-with -/// https://github.com/google/blockly/wiki/Lists#create-list-with]. +/// block text - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#create-list-with +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#create-list-with]. ///\n\nParameters:\n* %1 - the item (text) to be repeated\n* %2 - the number of times to repeat it Blockly.Msg.LISTS_REPEAT_TITLE = 'create list with item %1 repeated %2 times'; /** @type {string} */ /// {{Optional}} url - Information about how the length of a list is computed (i.e., by the total number of elements, not the number of different elements). -Blockly.Msg.LISTS_LENGTH_HELPURL = 'https://github.com/google/blockly/wiki/Lists#length-of'; +Blockly.Msg.LISTS_LENGTH_HELPURL = 'https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#length-of'; /** @type {string} */ -/// block text - See [https://github.com/google/blockly/wiki/Lists#length-of https://github.com/google/blockly/wiki/Lists#length-of]. +/// block text - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#length-of https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#length-of]. /// \n\nParameters:\n* %1 - the list whose length is desired Blockly.Msg.LISTS_LENGTH_TITLE = 'length of %1'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Lists#length-of https://github.com/google/blockly/wiki/Lists#length-of Blockly:Lists:length of]. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#length-of https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#length-of Blockly:Lists:length of]. Blockly.Msg.LISTS_LENGTH_TOOLTIP = 'Returns the length of a list.'; /** @type {string} */ -/// {{Optional}} url - See [https://github.com/google/blockly/wiki/Lists#is-empty https://github.com/google/blockly/wiki/Lists#is-empty]. -Blockly.Msg.LISTS_ISEMPTY_HELPURL = 'https://github.com/google/blockly/wiki/Lists#is-empty'; +/// {{Optional}} url - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#is-empty https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#is-empty]. +Blockly.Msg.LISTS_ISEMPTY_HELPURL = 'https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#is-empty'; /** @type {string} */ -/// block text - See [https://github.com/google/blockly/wiki/Lists#is-empty -/// https://github.com/google/blockly/wiki/Lists#is-empty]. +/// block text - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#is-empty +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#is-empty]. /// \n\nParameters:\n* %1 - the list to test Blockly.Msg.LISTS_ISEMPTY_TITLE = '%1 is empty'; /** @type {string} */ -/// block tooltip - See [https://github.com/google/blockly/wiki/Lists#is-empty -/// https://github.com/google/blockly/wiki/Lists#is-empty]. +/// block tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#is-empty +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#is-empty]. Blockly.Msg.LISTS_ISEMPTY_TOOLTIP = 'Returns true if the list is empty.'; /** @type {string} */ -/// block text - Title of blocks operating on [https://github.com/google/blockly/wiki/Lists lists]. +/// block text - Title of blocks operating on [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists lists]. Blockly.Msg.LISTS_INLIST = 'in list'; /** @type {string} */ -/// {{Optional}} url - See [https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list -/// https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list]. -Blockly.Msg.LISTS_INDEX_OF_HELPURL = 'https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list'; +/// {{Optional}} url - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#finding-items-in-a-list +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#finding-items-in-a-list]. +Blockly.Msg.LISTS_INDEX_OF_HELPURL = 'https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#finding-items-in-a-list'; /** @type {string} */ Blockly.Msg.LISTS_INDEX_OF_INPUT_IN_LIST = Blockly.Msg.LISTS_INLIST; /** @type {string} */ -/// dropdown - See [https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list +/// dropdown - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#finding-items-in-a-list /// Lists#finding-items-in-a-list]. /// [[File:Blockly-list-find.png]] Blockly.Msg.LISTS_INDEX_OF_FIRST = 'find first occurrence of item'; /** @type {string} */ -/// dropdown - See [https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list -/// https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list]. +/// dropdown - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#finding-items-in-a-list +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#finding-items-in-a-list]. /// [[File:Blockly-list-find.png]] Blockly.Msg.LISTS_INDEX_OF_LAST = 'find last occurrence of item'; /** @type {string} */ -/// tooltip - %1 will be replaced by either the number 0 or -1 depending on the indexing mode. See [https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list -/// https://github.com/google/blockly/wiki/Lists#finding-items-in-a-list]. +/// tooltip - %1 will be replaced by either the number 0 or -1 depending on the indexing mode. See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#finding-items-in-a-list +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#finding-items-in-a-list]. /// [[File:Blockly-list-find.png]] Blockly.Msg.LISTS_INDEX_OF_TOOLTIP = 'Returns the index of the first/last occurrence of the item in the list. Returns %1 if item is not found.'; /** @type {string} */ -/// {{Optional}} url - See [https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list -/// https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list]. -Blockly.Msg.LISTS_GET_INDEX_HELPURL = 'https://github.com/google/blockly/wiki/Lists#getting-items-from-a-list'; +/// {{Optional}} url - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-items-from-a-list +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-items-from-a-list]. +Blockly.Msg.LISTS_GET_INDEX_HELPURL = 'https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-items-from-a-list'; /** @type {string} */ /// dropdown - Indicates that the user wishes to -/// [https://github.com/google/blockly/wiki/Lists#getting-a-single-item +/// [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item /// get an item from a list] without removing it from the list. Blockly.Msg.LISTS_GET_INDEX_GET = 'get'; /** @type {string} */ /// dropdown - Indicates that the user wishes to -/// [https://github.com/google/blockly/wiki/Lists#getting-a-single-item +/// [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item /// get and remove an item from a list], as opposed to merely getting /// it without modifying the list. Blockly.Msg.LISTS_GET_INDEX_GET_REMOVE = 'get and remove'; /** @type {string} */ /// dropdown - Indicates that the user wishes to -/// [https://github.com/google/blockly/wiki/Lists#removing-an-item +/// [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#removing-an-item /// remove an item from a list].\n{{Identical|Remove}} Blockly.Msg.LISTS_GET_INDEX_REMOVE = 'remove'; /** @type {string} */ /// dropdown - Indicates that an index relative to the front of the list should be used to -/// [https://github.com/google/blockly/wiki/Lists#getting-a-single-item get and/or remove +/// [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item get and/or remove /// an item from a list]. Note: If {{msg-blockly|ORDINAL_NUMBER_SUFFIX}} is defined, it will /// automatically appear ''after'' this number (and any other ordinal numbers on this block). /// See [[Translating:Blockly#Ordinal_numbers]] for more information on ordinal numbers in Blockly. @@ -1205,28 +1208,28 @@ Blockly.Msg.LISTS_GET_INDEX_REMOVE = 'remove'; Blockly.Msg.LISTS_GET_INDEX_FROM_START = '#'; /** @type {string} */ /// dropdown - Indicates that an index relative to the end of the list should be used -/// to [https://github.com/google/blockly/wiki/Lists#getting-a-single-item access an item in a list]. +/// to [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item access an item in a list]. /// [[File:Blockly-list-get-item.png]] Blockly.Msg.LISTS_GET_INDEX_FROM_END = '# from end'; /** @type {string} */ /// dropdown - Indicates that the '''first''' item should be -/// [https://github.com/google/blockly/wiki/Lists#getting-a-single-item accessed in a list]. +/// [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item accessed in a list]. /// [[File:Blockly-list-get-item.png]] Blockly.Msg.LISTS_GET_INDEX_FIRST = 'first'; /** @type {string} */ /// dropdown - Indicates that the '''last''' item should be -/// [https://github.com/google/blockly/wiki/Lists#getting-a-single-item accessed in a list]. +/// [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item accessed in a list]. /// [[File:Blockly-list-get-item.png]] Blockly.Msg.LISTS_GET_INDEX_LAST = 'last'; /** @type {string} */ /// dropdown - Indicates that a '''random''' item should be -/// [https://github.com/google/blockly/wiki/Lists#getting-a-single-item accessed in a list]. +/// [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item accessed in a list]. /// [[File:Blockly-list-get-item.png]] Blockly.Msg.LISTS_GET_INDEX_RANDOM = 'random'; /** @type {string} */ /// {{Optional|Supply translation only if your language requires it. Most do not.}} /// block text - Text that should go after the rightmost block/dropdown when -/// [https://github.com/google/blockly/wiki/Lists#getting-a-single-item +/// [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item /// accessing an item from a list]. In most languages, this will be the empty string. /// [[File:Blockly-list-get-item.png]] Blockly.Msg.LISTS_GET_INDEX_TAIL = ''; @@ -1239,53 +1242,53 @@ Blockly.Msg.LISTS_INDEX_FROM_START_TOOLTIP = '%1 is the first item.'; /// tooltip - Indicates the ordinal number that the last item in a list is referenced by. %1 will be replaced by either "#0" or "#1" depending on the indexing mode. Blockly.Msg.LISTS_INDEX_FROM_END_TOOLTIP = '%1 is the last item.'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-a-single-item https://github.com/google/blockly/wiki/Lists#getting-a-single-item] for more information. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item] for more information. Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_GET_FROM = 'Returns the item at the specified position in a list.'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-a-single-item https://github.com/google/blockly/wiki/Lists#getting-a-single-item] for more information. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item] for more information. Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_GET_FIRST = 'Returns the first item in a list.'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-a-single-item https://github.com/google/blockly/wiki/Lists#getting-a-single-item] for more information. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item] for more information. Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_GET_LAST = 'Returns the last item in a list.'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-a-single-item https://github.com/google/blockly/wiki/Lists#getting-a-single-item] for more information. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item] for more information. Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_GET_RANDOM = 'Returns a random item in a list.'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-and-removing-an-item] (for remove and return) and [https://github.com/google/blockly/wiki/Lists#getting-a-single-item] for '#' or '# from end'. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-and-removing-an-item] (for remove and return) and [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item] for '#' or '# from end'. Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_FROM = 'Removes and returns the item at the specified position in a list.'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-and-removing-an-item] (for remove and return) and [https://github.com/google/blockly/wiki/Lists#getting-a-single-item] for 'first'. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-and-removing-an-item] (for remove and return) and [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item] for 'first'. Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_FIRST = 'Removes and returns the first item in a list.'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-and-removing-an-item] (for remove and return) and [https://github.com/google/blockly/wiki/Lists#getting-a-single-item] for 'last'. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-and-removing-an-item] (for remove and return) and [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item] for 'last'. Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_LAST = 'Removes and returns the last item in a list.'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-and-removing-an-item] (for remove and return) and [https://github.com/google/blockly/wiki/Lists#getting-a-single-item] for 'random'. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-and-removing-an-item] (for remove and return) and [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item] for 'random'. Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_GET_REMOVE_RANDOM = 'Removes and returns a random item in a list.'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-and-removing-an-item] (for remove and return) and [https://github.com/google/blockly/wiki/Lists#getting-a-single-item] for '#' or '# from end'. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-and-removing-an-item] (for remove and return) and [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item] for '#' or '# from end'. Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_REMOVE_FROM = 'Removes the item at the specified position in a list.'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-and-removing-an-item] (for remove and return) and [https://github.com/google/blockly/wiki/Lists#getting-a-single-item] for 'first'. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-and-removing-an-item] (for remove and return) and [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item] for 'first'. Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_REMOVE_FIRST = 'Removes the first item in a list.'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-and-removing-an-item] (for remove and return) and [https://github.com/google/blockly/wiki/Lists#getting-a-single-item] for 'last'. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-and-removing-an-item] (for remove and return) and [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item] for 'last'. Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_REMOVE_LAST = 'Removes the last item in a list.'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-and-removing-an-item] (for remove and return) and [https://github.com/google/blockly/wiki/Lists#getting-a-single-item] for 'random'. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-and-removing-an-item] (for remove and return) and [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item] for 'random'. Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_REMOVE_RANDOM = 'Removes a random item in a list.'; /** @type {string} */ /// {{Optional}} url - Information about putting items in lists. -Blockly.Msg.LISTS_SET_INDEX_HELPURL = 'https://github.com/google/blockly/wiki/Lists#in-list--set'; +Blockly.Msg.LISTS_SET_INDEX_HELPURL = 'https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#in-list--set'; /** @type {string} */ Blockly.Msg.LISTS_SET_INDEX_INPUT_IN_LIST = Blockly.Msg.LISTS_INLIST; /** @type {string} */ -/// block text - [https://github.com/google/blockly/wiki/Lists#in-list--set +/// block text - [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#in-list--set /// Replaces an item in a list]. /// [[File:Blockly-in-list-set-insert.png]] Blockly.Msg.LISTS_SET_INDEX_SET = 'set'; /** @type {string} */ -/// block text - [https://github.com/google/blockly/wiki/Lists#in-list--insert-at +/// block text - [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#in-list--insert-at /// Inserts an item into a list]. /// [[File:Blockly-in-list-set-insert.png]] Blockly.Msg.LISTS_SET_INDEX_INSERT = 'insert at'; @@ -1294,39 +1297,39 @@ Blockly.Msg.LISTS_SET_INDEX_INSERT = 'insert at'; /// [[File:Blockly-in-list-set-insert.png]] Blockly.Msg.LISTS_SET_INDEX_INPUT_TO = 'as'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-a-single-item} (even though the page describes the "get" block, the idea is the same for the "set" block). +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item} (even though the page describes the "get" block, the idea is the same for the "set" block). Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_SET_FROM = 'Sets the item at the specified position in a list.'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-a-single-item} (even though the page describes the "get" block, the idea is the same for the "set" block). +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item} (even though the page describes the "get" block, the idea is the same for the "set" block). Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_SET_FIRST = 'Sets the first item in a list.'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-a-single-item} (even though the page describes the "get" block, the idea is the same for the "set" block). +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item} (even though the page describes the "get" block, the idea is the same for the "set" block). Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_SET_LAST = 'Sets the last item in a list.'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-a-single-item} (even though the page describes the "get" block, the idea is the same for the "set" block). +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item} (even though the page describes the "get" block, the idea is the same for the "set" block). Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_SET_RANDOM = 'Sets a random item in a list.'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-a-single-item} (even though the page describes the "get" block, the idea is the same for the "insert" block). +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item} (even though the page describes the "get" block, the idea is the same for the "insert" block). Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_INSERT_FROM = 'Inserts the item at the specified position in a list.'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-a-single-item} (even though the page describes the "get" block, the idea is the same for the "insert" block). +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item} (even though the page describes the "get" block, the idea is the same for the "insert" block). Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_INSERT_FIRST = 'Inserts the item at the start of a list.'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-a-single-item} (even though the page describes the "get" block, the idea is the same for the "insert" block). +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item} (even though the page describes the "get" block, the idea is the same for the "insert" block). Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_INSERT_LAST = 'Append the item to the end of a list.'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-a-single-item} (even though the page describes the "get" block, the idea is the same for the "insert" block). +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-single-item} (even though the page describes the "get" block, the idea is the same for the "insert" block). Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_INSERT_RANDOM = 'Inserts the item randomly in a list.'; /** @type {string} */ /// {{Optional}} url - Information describing extracting a sublist from an existing list. -Blockly.Msg.LISTS_GET_SUBLIST_HELPURL = 'https://github.com/google/blockly/wiki/Lists#getting-a-sublist'; +Blockly.Msg.LISTS_GET_SUBLIST_HELPURL = 'https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-sublist'; /** @type {string} */ Blockly.Msg.LISTS_GET_SUBLIST_INPUT_IN_LIST = Blockly.Msg.LISTS_INLIST; /** @type {string} */ /// dropdown - Indicates that an index relative to the front of the list should be used /// to specify the beginning of the range from which to -/// [https://github.com/google/blockly/wiki/Lists#getting-a-sublist get a sublist]. +/// [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-sublist get a sublist]. /// [[File:Blockly-get-sublist.png]] /// Note: If {{msg-blockly|ORDINAL_NUMBER_SUFFIX}} is defined, it will /// automatically appear ''after'' this number (and any other ordinal numbers on this block). @@ -1335,28 +1338,28 @@ Blockly.Msg.LISTS_GET_SUBLIST_START_FROM_START = 'get sub-list from #'; /** @type {string} */ /// dropdown - Indicates that an index relative to the end of the list should be used /// to specify the beginning of the range from which to -/// [https://github.com/google/blockly/wiki/Lists#getting-a-sublist get a sublist]. +/// [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-sublist get a sublist]. Blockly.Msg.LISTS_GET_SUBLIST_START_FROM_END = 'get sub-list from # from end'; /** @type {string} */ /// dropdown - Indicates that the -/// [https://github.com/google/blockly/wiki/Lists#getting-a-sublist sublist to extract] +/// [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-sublist sublist to extract] /// should begin with the list's first item. Blockly.Msg.LISTS_GET_SUBLIST_START_FIRST = 'get sub-list from first'; /** @type {string} */ /// dropdown - Indicates that an index relative to the front of the list should be /// used to specify the end of the range from which to -/// [https://github.com/google/blockly/wiki/Lists#getting-a-sublist get a sublist]. +/// [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-sublist get a sublist]. /// [[File:Blockly-get-sublist.png]] Blockly.Msg.LISTS_GET_SUBLIST_END_FROM_START = 'to #'; /** @type {string} */ /// dropdown - Indicates that an index relative to the end of the list should be /// used to specify the end of the range from which to -/// [https://github.com/google/blockly/wiki/Lists#getting-a-sublist get a sublist]. +/// [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-sublist get a sublist]. /// [[File:Blockly-get-sublist.png]] Blockly.Msg.LISTS_GET_SUBLIST_END_FROM_END = 'to # from end'; /** @type {string} */ /// dropdown - Indicates that the '''last''' item in the given list should be -/// [https://github.com/google/blockly/wiki/Lists#getting-a-sublist the end +/// [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-sublist the end /// of the selected sublist]. /// [[File:Blockly-get-sublist.png]] Blockly.Msg.LISTS_GET_SUBLIST_END_LAST = 'to last'; @@ -1364,25 +1367,25 @@ Blockly.Msg.LISTS_GET_SUBLIST_END_LAST = 'to last'; /// {{Optional|Supply translation only if your language requires it. Most do not.}} /// block text - This appears in the rightmost position ("tail") of the /// sublist block, as described at -/// [https://github.com/google/blockly/wiki/Lists#getting-a-sublist -/// https://github.com/google/blockly/wiki/Lists#getting-a-sublist]. +/// [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-sublist +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-sublist]. /// In English and most other languages, this is the empty string. /// [[File:Blockly-get-sublist.png]] Blockly.Msg.LISTS_GET_SUBLIST_TAIL = ''; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Lists#getting-a-sublist -/// https://github.com/google/blockly/wiki/Lists#getting-a-sublist] for more information. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-sublist +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#getting-a-sublist] for more information. /// [[File:Blockly-get-sublist.png]] Blockly.Msg.LISTS_GET_SUBLIST_TOOLTIP = 'Creates a copy of the specified portion of a list.'; /** @type {string} */ /// {{Optional}} url - Information describing sorting a list. -Blockly.Msg.LISTS_SORT_HELPURL = 'https://github.com/google/blockly/wiki/Lists#sorting-a-list'; +Blockly.Msg.LISTS_SORT_HELPURL = 'https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#sorting-a-list'; /** @type {string} */ /// Sort as type %1 (numeric or alphabetic) in order %2 (ascending or descending) a list of items %3.\n{{Identical|Sort}} Blockly.Msg.LISTS_SORT_TITLE = 'sort %1 %2 %3'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Lists#sorting-a-list]. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#sorting-a-list]. Blockly.Msg.LISTS_SORT_TOOLTIP = 'Sort a copy of a list.'; /** @type {string} */ /// sorting order or direction from low to high value for numeric, or A-Z for alphabetic.\n{{Identical|Ascending}} @@ -1402,7 +1405,7 @@ Blockly.Msg.LISTS_SORT_TYPE_IGNORECASE = 'alphabetic, ignore case'; /** @type {string} */ /// {{Optional}} url - Information describing splitting text into a list, or joining a list into text. -Blockly.Msg.LISTS_SPLIT_HELPURL = 'https://github.com/google/blockly/wiki/Lists#splitting-strings-and-joining-lists'; +Blockly.Msg.LISTS_SPLIT_HELPURL = 'https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#splitting-strings-and-joining-lists'; /** @type {string} */ /// dropdown - Indicates that text will be split up into a list (e.g. "a-b-c" -> ["a", "b", "c"]). Blockly.Msg.LISTS_SPLIT_LIST_FROM_TEXT = 'make list from text'; @@ -1413,17 +1416,17 @@ Blockly.Msg.LISTS_SPLIT_TEXT_FROM_LIST = 'make text from list'; /// block text - Prompts for a letter to be used as a separator when splitting or joining text. Blockly.Msg.LISTS_SPLIT_WITH_DELIMITER = 'with delimiter'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Lists#make-list-from-text -/// https://github.com/google/blockly/wiki/Lists#make-list-from-text] for more information. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#make-list-from-text +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#make-list-from-text] for more information. Blockly.Msg.LISTS_SPLIT_TOOLTIP_SPLIT = 'Split text into a list of texts, breaking at each delimiter.'; /** @type {string} */ -/// tooltip - See [https://github.com/google/blockly/wiki/Lists#make-text-from-list -/// https://github.com/google/blockly/wiki/Lists#make-text-from-list] for more information. +/// tooltip - See [https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#make-text-from-list +/// https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#make-text-from-list] for more information. Blockly.Msg.LISTS_SPLIT_TOOLTIP_JOIN = 'Join a list of texts into one text, separated by a delimiter.'; /** @type {string} */ /// {{Optional}} url - Information describing reversing a list. -Blockly.Msg.LISTS_REVERSE_HELPURL = 'https://github.com/google/blockly/wiki/Lists#reversing-a-list'; +Blockly.Msg.LISTS_REVERSE_HELPURL = 'https://github.com/RaspberryPiFoundation/blockly/wiki/Lists#reversing-a-list'; /** @type {string} */ /// block text - Title of block that returns a copy of a list (%1) with the order of items reversed. Blockly.Msg.LISTS_REVERSE_MESSAGE0 = 'reverse %1'; @@ -1442,7 +1445,7 @@ Blockly.Msg.ORDINAL_NUMBER_SUFFIX = ''; // Variables Blocks. /** @type {string} */ /// {{Optional}} url - Information about ''variables'' in computer programming. Consider using your language's translation of [https://en.wikipedia.org/wiki/Variable_(computer_science) https://en.wikipedia.org/wiki/Variable_(computer_science)], if it exists. -Blockly.Msg.VARIABLES_GET_HELPURL = 'https://github.com/google/blockly/wiki/Variables#get'; +Blockly.Msg.VARIABLES_GET_HELPURL = 'https://github.com/RaspberryPiFoundation/blockly/wiki/Variables#get'; /** @type {string} */ /// tooltip - This gets the value of the named variable without modifying it. Blockly.Msg.VARIABLES_GET_TOOLTIP = 'Returns the value of this variable.'; @@ -1453,7 +1456,7 @@ Blockly.Msg.VARIABLES_GET_CREATE_SET = 'Create "set %1"'; /** @type {string} */ /// {{Optional}} url - Information about ''variables'' in computer programming. Consider using your language's translation of [https://en.wikipedia.org/wiki/Variable_(computer_science) https://en.wikipedia.org/wiki/Variable_(computer_science)], if it exists. -Blockly.Msg.VARIABLES_SET_HELPURL = 'https://github.com/google/blockly/wiki/Variables#set'; +Blockly.Msg.VARIABLES_SET_HELPURL = 'https://github.com/RaspberryPiFoundation/blockly/wiki/Variables#set'; /** @type {string} */ /// block text - Change the value of a mathematical variable: '''set [the value of] x to 7'''.\n\nParameters:\n* %1 - the name of the variable.\n* %2 - the value to be assigned. Blockly.Msg.VARIABLES_SET = 'set %1 to %2'; @@ -1614,3 +1617,79 @@ Blockly.Msg.DIALOG_OK = 'OK'; /** @type {string} */ /// button label - Pressing this button cancels a proposed action.\n{{Identical|Cancel}} Blockly.Msg.DIALOG_CANCEL = 'Cancel'; + +/** @type {string} */ +/// menu label - Contextual menu item that moves the keyboard navigation cursor +/// into a subitem of the focused block. +Blockly.Msg.EDIT_BLOCK_CONTENTS = 'Edit Block contents'; +/** @type {string} */ +/// menu label - Contextual menu item that starts a keyboard-driven block move. +Blockly.Msg.MOVE_BLOCK = 'Move Block'; +/** @type {string} */ +/// Name of the Microsoft Windows operating system displayed in a list of +/// keyboard shortcuts. +Blockly.Msg.WINDOWS = 'Windows'; +/** @type {string} */ +/// Name of the Apple macOS operating system displayed in a list of keyboard +/// shortcuts, +Blockly.Msg.MAC_OS = 'macOS'; +/** @type {string} */ +/// Name of the Google ChromeOS operating system displayed in a list of keyboard +/// shortcuts. +Blockly.Msg.CHROME_OS = 'ChromeOS'; +/** @type {string} */ +/// Name of the GNU/Linux operating system displayed in a list of keyboard +/// shortcuts. +Blockly.Msg.LINUX = 'Linux'; +/** @type {string} */ +/// Placeholder name for an operating system that can't be identified in a list +/// of keyboard shortcuts. +Blockly.Msg.UNKNOWN = 'Unknown'; +/** @type {string} */ +/// Representation of the Control key used in keyboard shortcuts. +Blockly.Msg.CONTROL_KEY = 'Ctrl'; +/** @type {string} */ +/// Representation of the Mac Command key used in keyboard shortcuts. +Blockly.Msg.COMMAND_KEY = '⌘ Command'; +/** @type {string} */ +/// Representation of the Mac Option key used in keyboard shortcuts. +Blockly.Msg.OPTION_KEY = '⌥ Option'; +/** @type {string} */ +/// Representation of the Alt key used in keyboard shortcuts. +Blockly.Msg.ALT_KEY = 'Alt'; +/** @type {string} */ +/// menu label - Contextual menu item that cuts the focused item. +Blockly.Msg.CUT_SHORTCUT = 'Cut'; +/** @type {string} */ +/// menu label - Contextual menu item that copies the focused item. +Blockly.Msg.COPY_SHORTCUT = 'Copy'; +/** @type {string} */ +/// menu label - Contextual menu item that pastes the previously copied item. +Blockly.Msg.PASTE_SHORTCUT = 'Paste'; +/** @type {string} */ +/// Alert message shown to prompt users to review available keyboard shortcuts. +Blockly.Msg.HELP_PROMPT = 'Press %1 for help on keyboard controls'; +/** @type {string} */ +/// shortcut list section header - Label for general purpose keyboard shortcuts. +Blockly.Msg.SHORTCUTS_GENERAL = 'General'; +/** @type {string} */ +/// shortcut list section header - Label for keyboard shortcuts related to +/// editing a workspace. +Blockly.Msg.SHORTCUTS_EDITING = 'Editing' +/** @type {string} */ +/// shortcut list section header - Label for keyboard shortcuts related to +/// moving around the workspace. +Blockly.Msg.SHORTCUTS_CODE_NAVIGATION = 'Code navigation'; +/** @type {string} */ +/// Message shown to inform users how to move blocks to arbitrary locations +/// with the keyboard. +Blockly.Msg.KEYBOARD_NAV_UNCONSTRAINED_MOVE_HINT = 'Hold %1 and use arrow keys to move freely, then %2 to accept the position'; +/** @type {string} */ +/// Message shown to inform users how to move blocks with the keyboard. +Blockly.Msg.KEYBOARD_NAV_CONSTRAINED_MOVE_HINT = 'Use the arrow keys to move, then %1 to accept the position'; +/** @type {string} */ +/// Message shown when an item is copied in keyboard navigation mode. +Blockly.Msg.KEYBOARD_NAV_COPIED_HINT = 'Copied. Press %1 to paste.'; +/** @type {string} */ +/// Message shown when an item is cut in keyboard navigation mode. +Blockly.Msg.KEYBOARD_NAV_CUT_HINT = 'Cut. Press %1 to paste.'; \ No newline at end of file diff --git a/packages/blockly/package.json b/packages/blockly/package.json new file mode 100644 index 00000000000..ecd6100583e --- /dev/null +++ b/packages/blockly/package.json @@ -0,0 +1,156 @@ +{ + "name": "blockly", + "version": "12.4.1-mit-appinventor.1", + "description": "Blockly is a library for building visual programming editors.", + "keywords": [ + "blockly" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/google/blockly.git" + }, + "bugs": { + "url": "https://github.com/google/blockly/issues" + }, + "homepage": "https://developers.google.com/blockly/", + "author": { + "name": "Neil Fraser" + }, + "scripts": { + "build": "gulp build", + "build-debug": "gulp build --verbose --debug", + "build-debug-log": "npm run build:debug > build-debug.log 2>&1 && tail -3 build-debug.log", + "build-strict": "gulp build --verbose --strict", + "build-strict-log": "npm run build:strict > build-debug.log 2>&1 && tail -3 build-debug.log", + "clean": "gulp clean", + "deployDemos": "npm ci && gulp deployDemos", + "deployDemos:beta": "npm ci && gulp deployDemosBeta", + "docs": "gulp docs", + "format": "prettier --write .", + "format:check": "prettier --check .", + "messages": "gulp messages", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "langfiles": "gulp langfiles", + "minify": "gulp minify", + "package": "gulp pack", + "postinstall": "patch-package", + "prepareDemos": "gulp prepareDemos", + "publish": "npm ci && gulp publish", + "publish:beta": "npm ci && gulp publishBeta", + "recompile": "gulp recompile", + "release": "gulp gitCreateRC", + "start": "npm run build && concurrently -n tsc,server \"tsc --watch --preserveWatchOutput --outDir \"build/src\" --declarationDir \"build/declarations\"\" \"http-server ./ -s -o /tests/playground.html -c-1\"", + "tsc": "gulp tsc", + "test": "gulp test", + "test:browser": "npx mocha --config tests/browser/.mocharc.js", + "test:generators": "gulp testGenerators", + "test:mocha:interactive": "npm run build && concurrently -n tsc,server \"tsc --watch --preserveWatchOutput --outDir \"build/src\" --declarationDir \"build/declarations\"\" \"gulp interactiveMocha\"", + "test:compile:advanced": "gulp buildAdvancedCompilationTest --debug", + "updateGithubPages": "npm ci && gulp gitUpdateGithubPages" + }, + "exports": { + ".": { + "types": "./index.d.ts", + "import": "./index.mjs", + "umd": "./blockly.min.js", + "default": "./index.js" + }, + "./core": { + "types": "./core.d.ts", + "node": "./core-node.js", + "import": "./blockly.mjs", + "default": "./blockly_compressed.js" + }, + "./blocks": { + "types": "./blocks.d.ts", + "import": "./blocks.mjs", + "default": "./blocks_compressed.js" + }, + "./dart": { + "types": "./dart.d.ts", + "import": "./dart.mjs", + "default": "./dart_compressed.js" + }, + "./lua": { + "types": "./lua.d.ts", + "import": "./lua.mjs", + "default": "./lua_compressed.js" + }, + "./javascript": { + "types": "./javascript.d.ts", + "import": "./javascript.mjs", + "default": "./javascript_compressed.js" + }, + "./php": { + "types": "./php.d.ts", + "import": "./php.mjs", + "default": "./php_compressed.js" + }, + "./python": { + "types": "./python.d.ts", + "import": "./python.mjs", + "default": "./python_compressed.js" + }, + "./msg/*": { + "types": "./msg/*.d.ts", + "import": "./msg/*.mjs", + "default": "./msg/*.js" + } + }, + "license": "Apache-2.0", + "devDependencies": { + "@blockly/block-test": "^7.0.2", + "@blockly/dev-tools": "^9.0.2", + "@blockly/keyboard-navigation": "^3.0.1", + "@blockly/theme-modern": "^7.0.1", + "@hyperjump/browser": "^1.1.4", + "@hyperjump/json-schema": "^1.5.0", + "@microsoft/api-documenter": "7.22.4", + "@microsoft/api-extractor": "^7.29.5", + "ajv": "^8.17.1", + "async-done": "^2.0.0", + "chai": "^6.0.1", + "concurrently": "^9.0.1", + "eslint": "^9.15.0", + "eslint-config-google": "^0.14.0", + "eslint-config-prettier": "^10.1.1", + "eslint-plugin-jsdoc": "^52.0.2", + "eslint-plugin-mocha": "^11.1.0", + "eslint-plugin-prettier": "^5.2.1", + "glob": "^11.0.1", + "globals": "^16.0.0", + "google-closure-compiler": "^20260114.0.0", + "gulp": "^5.0.0", + "gulp-concat": "^2.6.1", + "gulp-gzip": "^1.4.2", + "gulp-header": "^2.0.9", + "gulp-insert": "^0.5.0", + "gulp-rename": "^2.0.0", + "gulp-replace": "^1.0.0", + "gulp-series": "^1.0.2", + "gulp-shell": "^0.8.0", + "gulp-sourcemaps": "^3.0.0", + "gulp-umd": "^2.0.0", + "http-server": "^14.0.0", + "json5": "^2.2.0", + "markdown-tables-to-json": "^0.1.7", + "mocha": "^11.3.0", + "patch-package": "^8.0.0", + "prettier": "^3.3.3", + "prettier-plugin-organize-imports": "^4.0.0", + "puppeteer-core": "^24.17.0", + "readline-sync": "^1.4.10", + "rimraf": "^5.0.0", + "typescript": "^5.3.3", + "typescript-eslint": "^8.16.0", + "webdriverio": "^9.0.7", + "yargs": "^17.2.1" + }, + "dependencies": { + "jsdom": "26.1.0" + }, + "engines": { + "node": ">=18" + } +} diff --git a/scripts/gulpfiles/appengine_tasks.js b/packages/blockly/scripts/gulpfiles/appengine_tasks.mjs similarity index 86% rename from scripts/gulpfiles/appengine_tasks.js rename to packages/blockly/scripts/gulpfiles/appengine_tasks.mjs index ddbd2f45f90..ae7fd943b9e 100644 --- a/scripts/gulpfiles/appengine_tasks.js +++ b/packages/blockly/scripts/gulpfiles/appengine_tasks.mjs @@ -8,16 +8,16 @@ * @fileoverview Gulp script to deploy Blockly demos on appengine. */ -const gulp = require('gulp'); +import * as gulp from 'gulp'; -const fs = require('fs'); -const path = require('path'); -const execSync = require('child_process').execSync; -const buildTasks = require('./build_tasks.js'); -const packageTasks = require('./package_tasks.js'); -const {rimraf} = require('rimraf'); +import * as fs from 'fs'; +import * as path from 'path'; +import {execSync} from 'child_process'; +import * as buildTasks from './build_tasks.mjs'; +import {getPackageJson} from './helper_tasks.mjs'; +import * as packageTasks from './package_tasks.mjs'; +import {rimraf} from 'rimraf'; -const packageJson = require('../../package.json'); const demoTmpDir = '../_deploy'; const demoStaticTmpDir = '../_deploy/static'; @@ -98,6 +98,7 @@ function copyPlaygroundDeps() { './node_modules/@blockly/dev-tools/dist/index.js', './node_modules/@blockly/theme-modern/dist/index.js', './node_modules/@blockly/block-test/dist/index.js', + './node_modules/@blockly/keyboard-navigation/dist/index.js', ]; return gulp.src(playgroundDeps, {base: '.'}).pipe(gulp.dest(demoStaticTmpDir)); } @@ -123,7 +124,7 @@ function deployToAndClean(demoVersion) { */ function getDemosVersion() { // Replace all '.' with '-' e.g. 9-3-3-beta-2 - return packageJson.version.replace(/\./g, '-'); + return getPackageJson().version.replace(/\./g, '-'); } /** @@ -162,7 +163,7 @@ function deployBetaAndClean(done) { * * Prerequisites (invoked): clean, build */ -const prepareDemos = gulp.series( +export const prepareDemos = gulp.series( prepareDeployDir, gulp.parallel( gulp.series( @@ -180,16 +181,9 @@ const prepareDemos = gulp.series( /** * Deploys demos. */ -const deployDemos = gulp.series(prepareDemos, deployAndClean); +export const deployDemos = gulp.series(prepareDemos, deployAndClean); /** * Deploys beta version of demos (version appended with -beta). */ -const deployDemosBeta = gulp.series(prepareDemos, deployBetaAndClean); - -module.exports = { - // Main sequence targets. Each should invoke any immediate prerequisite(s). - deployDemos: deployDemos, - deployDemosBeta: deployDemosBeta, - prepareDemos: prepareDemos -}; +export const deployDemosBeta = gulp.series(prepareDemos, deployBetaAndClean); diff --git a/scripts/gulpfiles/build_tasks.js b/packages/blockly/scripts/gulpfiles/build_tasks.mjs similarity index 92% rename from scripts/gulpfiles/build_tasks.js rename to packages/blockly/scripts/gulpfiles/build_tasks.mjs index a00c1b17dc3..00e189e889f 100644 --- a/scripts/gulpfiles/build_tasks.js +++ b/packages/blockly/scripts/gulpfiles/build_tasks.mjs @@ -8,25 +8,28 @@ * @fileoverview Gulp script to build Blockly for Node & NPM. */ -const gulp = require('gulp'); -gulp.replace = require('gulp-replace'); -gulp.rename = require('gulp-rename'); -gulp.sourcemaps = require('gulp-sourcemaps'); +import * as gulp from 'gulp'; +import replace from 'gulp-replace'; +import rename from 'gulp-rename'; +import sourcemaps from 'gulp-sourcemaps'; -const path = require('path'); -const fs = require('fs'); -const fsPromises = require('fs/promises'); -const {exec, execSync} = require('child_process'); +import * as path from 'path'; +import * as fs from 'fs'; +import * as fsPromises from 'fs/promises'; +import {exec, execSync} from 'child_process'; -const {globSync} = require('glob'); -const closureCompiler = require('google-closure-compiler').gulp(); -const argv = require('yargs').argv; -const {rimraf} = require('rimraf'); +import {globSync} from 'glob'; +import {gulp as closureCompiler} from 'google-closure-compiler'; +import yargs from 'yargs'; +import {hideBin} from 'yargs/helpers'; +import {rimraf} from 'rimraf'; -const {BUILD_DIR, LANG_BUILD_DIR, RELEASE_DIR, TSC_OUTPUT_DIR, TYPINGS_BUILD_DIR} = require('./config'); -const {getPackageJson} = require('./helper_tasks'); +import {BUILD_DIR, LANG_BUILD_DIR, RELEASE_DIR, TSC_OUTPUT_DIR, TYPINGS_BUILD_DIR} from './config.mjs'; +import {getPackageJson} from './helper_tasks.mjs'; -const {posixPath, quote} = require('../helpers'); +import {posixPath, quote} from '../helpers.js'; + +const argv = yargs(hideBin(process.argv)).parse(); //////////////////////////////////////////////////////////// // Build // @@ -182,7 +185,7 @@ function stripApacheLicense() { // Closure Compiler preserves dozens of Apache licences in the Blockly code. // Remove these if they belong to Google or MIT. // MIT's permission to do this is logged in Blockly issue #2412. - return gulp.replace(new RegExp(licenseRegex, 'g'), '\n\n\n\n'); + return replace(new RegExp(licenseRegex, 'g'), '\n\n\n\n'); // Replace with the same number of lines so that source-maps are not affected. } @@ -240,7 +243,6 @@ const JSCOMP_ERROR = [ 'underscore', 'unknownDefines', // 'unusedLocalVariables', // Disabled; see note in JSCOMP_OFF. - 'unusedPrivateMembers', 'uselessCode', 'untranspilableFeatures', // 'visibility', // Disabled; see note in JSCOMP_OFF. @@ -306,7 +308,7 @@ const JSCOMP_OFF = [ * Builds Blockly as a JS program, by running tsc on all the files in * the core directory. */ -function buildJavaScript(done) { +export function tsc(done) { execSync( `tsc -outDir "${TSC_OUTPUT_DIR}" -declarationDir "${TYPINGS_BUILD_DIR}"`, {stdio: 'inherit'}); @@ -318,7 +320,7 @@ function buildJavaScript(done) { * This task regenerates msg/json/en.js and msg/json/qqq.js from * msg/messages.js. */ -function generateMessages(done) { +export function messages(done) { // Run js_to_json.py const jsToJsonCmd = `${PYTHON} scripts/i18n/js_to_json.py \ --input_file ${path.join('msg', 'messages.js')} \ @@ -541,7 +543,7 @@ function compile(options) { } } // Extra options for Closure Compiler gulp plugin. - const platform = ['native', 'java', 'javascript']; + const platform = ['native']; return closureCompiler({...defaultOptions, ...options}, {platform}); } @@ -573,10 +575,10 @@ function buildCompiled() { // Fire up compilation pipline. return gulp.src(chunkOptions.js, {base: './'}) .pipe(stripApacheLicense()) - .pipe(gulp.sourcemaps.init()) + .pipe(sourcemaps.init()) .pipe(compile(options)) - .pipe(gulp.rename({suffix: COMPILED_SUFFIX})) - .pipe(gulp.sourcemaps.write('.')) + .pipe(rename({suffix: COMPILED_SUFFIX})) + .pipe(sourcemaps.write('.')) .pipe(gulp.dest(RELEASE_DIR)); } @@ -668,7 +670,7 @@ async function buildLangfileShims() { // (We have to do it this way because messages.js is a script and // not a CJS module with exports.) globalThis.Blockly = {Msg: {}}; - require('../../msg/messages.js'); + await import('../../msg/messages.js'); const exportedNames = Object.keys(globalThis.Blockly.Msg); delete globalThis.Blockly; @@ -689,12 +691,14 @@ ${exportedNames.map((name) => ` ${name},`).join('\n')} } /** - * This task builds Blockly core, blocks and generators together and uses - * Closure Compiler's ADVANCED_COMPILATION mode. + * This task uses Closure Compiler's ADVANCED_COMPILATION mode to + * compile together Blockly core, blocks and generators with a simple + * test app; the purpose is to verify that Blockly is compatible with + * the ADVANCED_COMPILATION mode. * * Prerequisite: buildJavaScript. */ -function buildAdvancedCompilationTest() { +function compileAdvancedCompilationTest() { // If main_compressed.js exists (from a previous run) delete it so that // a later browser-based test won't check it should the compile fail. try { @@ -718,9 +722,9 @@ function buildAdvancedCompilationTest() { }; return gulp.src(srcs, {base: './'}) .pipe(stripApacheLicense()) - .pipe(gulp.sourcemaps.init()) + .pipe(sourcemaps.init()) .pipe(compile(options)) - .pipe(gulp.sourcemaps.write( + .pipe(sourcemaps.write( '.', {includeContent: false, sourceRoot: '../../'})) .pipe(gulp.dest('./tests/compile/')); } @@ -728,7 +732,7 @@ function buildAdvancedCompilationTest() { /** * This task cleans the build directory (by deleting it). */ -function cleanBuildDir() { +export function cleanBuildDir() { // Sanity check. if (BUILD_DIR === '.' || BUILD_DIR === '/') { return Promise.reject(`Refusing to rm -rf ${BUILD_DIR}`); @@ -737,16 +741,13 @@ function cleanBuildDir() { } // Main sequence targets. Each should invoke any immediate prerequisite(s). -exports.cleanBuildDir = cleanBuildDir; -exports.langfiles = gulp.parallel(buildLangfiles, buildLangfileShims); -exports.tsc = buildJavaScript; -exports.minify = gulp.series(exports.tsc, buildCompiled, buildShims); -exports.build = gulp.parallel(exports.minify, exports.langfiles); +// function cleanBuildDir, above +export const langfiles = gulp.parallel(buildLangfiles, buildLangfileShims); +export const minify = gulp.series(tsc, buildCompiled, buildShims); +// function tsc, above +export const build = gulp.parallel(minify, langfiles); // Manually-invokable targets, with prerequisites where required. -exports.messages = generateMessages; // Generate msg/json/en.json et al. -exports.buildAdvancedCompilationTest = - gulp.series(exports.tsc, buildAdvancedCompilationTest); - -// Targets intended only for invocation by scripts; may omit prerequisites. -exports.onlyBuildAdvancedCompilationTest = buildAdvancedCompilationTest; +// function messages, above +export const buildAdvancedCompilationTest = + gulp.series(tsc, compileAdvancedCompilationTest); diff --git a/scripts/gulpfiles/config.js b/packages/blockly/scripts/gulpfiles/config.mjs similarity index 67% rename from scripts/gulpfiles/config.js rename to packages/blockly/scripts/gulpfiles/config.mjs index 90cd571099d..9d461231a87 100644 --- a/scripts/gulpfiles/config.js +++ b/packages/blockly/scripts/gulpfiles/config.mjs @@ -8,7 +8,7 @@ * @fileoverview Common configuration for Gulp scripts. */ -const path = require('path'); +import * as path from 'path'; // Paths are all relative to the repository root. Do not include // trailing slash. @@ -16,26 +16,25 @@ const path = require('path'); // TODO(#5007): If you modify these values, you must also modify the // corresponding values in the following files: // -// - tests/scripts/compile_typings.sh // - tests/scripts/check_metadata.sh // - tests/scripts/update_metadata.sh // Directory to write compiled output to. -exports.BUILD_DIR = 'build'; +export const BUILD_DIR = 'build'; // Directory to write typings output to. -exports.TYPINGS_BUILD_DIR = path.join(exports.BUILD_DIR, 'declarations'); +export const TYPINGS_BUILD_DIR = path.join(BUILD_DIR, 'declarations'); // Directory to write langfile output to. -exports.LANG_BUILD_DIR = path.join(exports.BUILD_DIR, 'msg'); +export const LANG_BUILD_DIR = path.join(BUILD_DIR, 'msg'); // Directory where typescript compiler output can be found. // Matches the value in tsconfig.json: outDir -exports.TSC_OUTPUT_DIR = path.join(exports.BUILD_DIR, 'src'); +export const TSC_OUTPUT_DIR = path.join(BUILD_DIR, 'src'); // Directory for files generated by compiling test code. -exports.TEST_TSC_OUTPUT_DIR = path.join(exports.BUILD_DIR, 'tests'); +export const TEST_TSC_OUTPUT_DIR = path.join(BUILD_DIR, 'tests'); // Directory in which to assemble (and from which to publish) the // blockly npm package. -exports.RELEASE_DIR = 'dist'; +export const RELEASE_DIR = 'dist'; diff --git a/scripts/gulpfiles/docs_tasks.js b/packages/blockly/scripts/gulpfiles/docs_tasks.mjs similarity index 94% rename from scripts/gulpfiles/docs_tasks.js rename to packages/blockly/scripts/gulpfiles/docs_tasks.mjs index 8820a586f9b..51abd480f95 100644 --- a/scripts/gulpfiles/docs_tasks.js +++ b/packages/blockly/scripts/gulpfiles/docs_tasks.mjs @@ -1,9 +1,9 @@ -const {execSync} = require('child_process'); -const {Extractor} = require('markdown-tables-to-json'); -const fs = require('fs'); -const gulp = require('gulp'); -const header = require('gulp-header'); -const replace = require('gulp-replace'); +import {execSync} from 'child_process'; +import {Extractor} from 'markdown-tables-to-json'; +import * as fs from 'fs'; +import * as gulp from 'gulp'; +import header from 'gulp-header'; +import replace from 'gulp-replace'; const DOCS_DIR = 'docs'; @@ -140,8 +140,7 @@ const createToc = function(done) { done(); } -const docs = gulp.series( +export const docs = gulp.series( generateApiJson, removeRenames, generateDocs, gulp.parallel(prependBook, createToc)); -module.exports = {docs}; diff --git a/scripts/gulpfiles/git_tasks.js b/packages/blockly/scripts/gulpfiles/git_tasks.mjs similarity index 86% rename from scripts/gulpfiles/git_tasks.js rename to packages/blockly/scripts/gulpfiles/git_tasks.mjs index 7c320cd8791..2b08e16b38b 100644 --- a/scripts/gulpfiles/git_tasks.js +++ b/packages/blockly/scripts/gulpfiles/git_tasks.mjs @@ -8,11 +8,11 @@ * @fileoverview Git-related gulp tasks for Blockly. */ -const gulp = require('gulp'); -const execSync = require('child_process').execSync; +import * as gulp from 'gulp'; +import {execSync} from 'child_process'; -const buildTasks = require('./build_tasks'); -const packageTasks = require('./package_tasks'); +import * as buildTasks from './build_tasks.mjs'; +import * as packageTasks from './package_tasks.mjs'; const UPSTREAM_URL = 'https://github.com/google/blockly.git'; @@ -63,7 +63,7 @@ function syncBranch(branchName) { * Stash current state, check out develop, and sync with * google/blockly. */ -function syncDevelop() { +export function syncDevelop() { return syncBranch('develop'); }; @@ -71,7 +71,7 @@ function syncDevelop() { * Stash current state, check out master, and sync with * google/blockly. */ -function syncMaster() { +export function syncMaster() { return syncBranch('master'); }; @@ -111,7 +111,7 @@ function checkoutBranch(branchName) { * Create and push an RC branch. * Note that this pushes to google/blockly. */ -const createRC = gulp.series( +export const createRC = gulp.series( syncDevelop(), function(done) { const branchName = getRCBranchName(); @@ -122,7 +122,7 @@ const createRC = gulp.series( ); /** Create the rebuild branch. */ -function createRebuildBranch(done) { +export function createRebuildBranch(done) { const branchName = getRebuildBranchName(); console.log(`make-rebuild-branch: creating branch ${branchName}`); execSync(`git switch -C ${branchName}`, { stdio: 'inherit' }); @@ -130,7 +130,7 @@ function createRebuildBranch(done) { } /** Push the rebuild branch to origin. */ -function pushRebuildBranch(done) { +export function pushRebuildBranch(done) { console.log('push-rebuild-branch: committing rebuild'); execSync('git commit -am "Rebuild"', { stdio: 'inherit' }); const branchName = getRebuildBranchName(); @@ -145,7 +145,7 @@ function pushRebuildBranch(done) { * * Prerequisites (invoked): clean, build. */ -const updateGithubPages = gulp.series( +export const updateGithubPages = gulp.series( function(done) { execSync('git stash save -m "Stash for sync"', { stdio: 'inherit' }); execSync('git switch -C gh-pages', { stdio: 'inherit' }); @@ -165,17 +165,3 @@ const updateGithubPages = gulp.series( done(); } ); - -module.exports = { - // Main sequence targets. Each should invoke any immediate prerequisite(s). - updateGithubPages, - - // Manually-invokable targets that invoke prerequisites. - createRC, - - // Legacy script-only targets, to be deleted. - syncDevelop, - syncMaster, - createRebuildBranch, - pushRebuildBranch, -}; diff --git a/packages/blockly/scripts/gulpfiles/helper_tasks.mjs b/packages/blockly/scripts/gulpfiles/helper_tasks.mjs new file mode 100644 index 00000000000..2068de106a5 --- /dev/null +++ b/packages/blockly/scripts/gulpfiles/helper_tasks.mjs @@ -0,0 +1,25 @@ +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview Any gulp helper functions. + */ + +import Module from "node:module"; + +const require = Module.createRequire(import.meta.url); + +/** + * Load and return the contents of package.json. + * + * Uses require() rather than import, and clears the require cache, to + * ensure the loaded package.json data is up to date. + */ +export function getPackageJson() { + delete require.cache[require.resolve('../../package.json')]; + return require('../../package.json'); +} + diff --git a/scripts/gulpfiles/package_tasks.js b/packages/blockly/scripts/gulpfiles/package_tasks.mjs similarity index 89% rename from scripts/gulpfiles/package_tasks.js rename to packages/blockly/scripts/gulpfiles/package_tasks.mjs index 89264a0e3c4..948f855b096 100644 --- a/scripts/gulpfiles/package_tasks.js +++ b/packages/blockly/scripts/gulpfiles/package_tasks.mjs @@ -8,20 +8,17 @@ * @fileoverview Gulp tasks to package Blockly for distribution on NPM. */ -const gulp = require('gulp'); -gulp.concat = require('gulp-concat'); -gulp.replace = require('gulp-replace'); -gulp.rename = require('gulp-rename'); -gulp.insert = require('gulp-insert'); -gulp.umd = require('gulp-umd'); -gulp.replace = require('gulp-replace'); +import * as gulp from 'gulp'; +import concat from 'gulp-concat'; +import replace from 'gulp-replace'; +import umd from 'gulp-umd'; -const path = require('path'); -const fs = require('fs'); -const {rimraf} = require('rimraf'); -const build = require('./build_tasks'); -const {getPackageJson} = require('./helper_tasks'); -const {BUILD_DIR, LANG_BUILD_DIR, RELEASE_DIR, TYPINGS_BUILD_DIR} = require('./config'); +import * as path from 'path'; +import * as fs from 'fs'; +import {rimraf} from 'rimraf'; +import * as build from './build_tasks.mjs'; +import {getPackageJson} from './helper_tasks.mjs'; +import {BUILD_DIR, LANG_BUILD_DIR, RELEASE_DIR, TYPINGS_BUILD_DIR} from './config.mjs'; // Path to template files for gulp-umd. const TEMPLATE_DIR = 'scripts/package/templates'; @@ -32,7 +29,7 @@ const TEMPLATE_DIR = 'scripts/package/templates'; * @param {Array} dependencies An array of dependencies to inject. */ function packageUMD(namespace, dependencies, template = 'umd.template') { - return gulp.umd({ + return umd({ dependencies: function () { return dependencies; }, namespace: function () { return namespace; }, exports: function () { return namespace; }, @@ -88,7 +85,7 @@ function packageCoreNode() { function packageLocales() { // Remove references to goog.provide and goog.require. return gulp.src(`${LANG_BUILD_DIR}/*.js`) - .pipe(gulp.replace(/goog\.[^\n]+/g, '')) + .pipe(replace(/goog\.[^\n]+/g, '')) .pipe(packageUMD('Blockly.Msg', [], 'umd-msg.template')) .pipe(gulp.dest(`${RELEASE_DIR}/msg`)); }; @@ -107,7 +104,7 @@ function packageUMDBundle() { `${RELEASE_DIR}/javascript_compressed.js`, ]; return gulp.src(srcs) - .pipe(gulp.concat('blockly.min.js')) + .pipe(concat('blockly.min.js')) .pipe(gulp.dest(`${RELEASE_DIR}`)); }; @@ -140,7 +137,7 @@ function packageUMDBundle() { * @param {Function} done Callback to call when done. */ function packageLegacyEntrypoints(done) { - for (entrypoint of [ + for (const entrypoint of [ 'core', 'blocks', 'dart', 'javascript', 'lua', 'php', 'python' ]) { const bundle = @@ -218,14 +215,14 @@ function packageDTS() { .pipe(gulp.src(`${TYPINGS_BUILD_DIR}/**/*.d.ts`, {ignore: [ `${TYPINGS_BUILD_DIR}/blocks/**/*`, ]})) - .pipe(gulp.replace('AnyDuringMigration', 'any')) + .pipe(replace('AnyDuringMigration', 'any')) .pipe(gulp.dest(RELEASE_DIR)); }; /** * This task cleans the release directory (by deleting it). */ -function cleanReleaseDir() { +export function cleanReleaseDir() { // Sanity check. if (RELEASE_DIR === '.' || RELEASE_DIR === '/') { return Promise.reject(`Refusing to rm -rf ${RELEASE_DIR}`); @@ -237,9 +234,13 @@ function cleanReleaseDir() { * This task prepares the files to be included in the NPM by copying * them into the release directory. * + * This task was formerly called "package" but was renamed in + * preparation for porting gulpfiles to ESM because "package" is a + * reserved word. + * * Prerequisite: build. */ -const package = gulp.series( +export const pack = gulp.series( gulp.parallel( build.cleanBuildDir, cleanReleaseDir), @@ -254,9 +255,3 @@ const package = gulp.series( packageReadme, packageDTS) ); - -module.exports = { - // Main sequence targets. Each should invoke any immediate prerequisite(s). - cleanReleaseDir: cleanReleaseDir, - package: package, -}; diff --git a/scripts/gulpfiles/release_tasks.js b/packages/blockly/scripts/gulpfiles/release_tasks.mjs similarity index 87% rename from scripts/gulpfiles/release_tasks.js rename to packages/blockly/scripts/gulpfiles/release_tasks.mjs index f2545c7b92b..a678a4f2436 100644 --- a/scripts/gulpfiles/release_tasks.js +++ b/packages/blockly/scripts/gulpfiles/release_tasks.mjs @@ -8,15 +8,15 @@ * @fileoverview Gulp scripts for releasing Blockly. */ -const execSync = require('child_process').execSync; -const fs = require('fs'); -const gulp = require('gulp'); -const readlineSync = require('readline-sync'); +import {execSync} from 'child_process'; +import * as fs from 'fs'; +import * as gulp from 'gulp'; +import * as readlineSync from 'readline-sync'; -const gitTasks = require('./git_tasks'); -const packageTasks = require('./package_tasks'); -const {getPackageJson} = require('./helper_tasks'); -const {RELEASE_DIR} = require('./config'); +import * as gitTasks from './git_tasks.mjs'; +import * as packageTasks from './package_tasks.mjs'; +import {getPackageJson} from './helper_tasks.mjs'; +import {RELEASE_DIR} from './config.mjs'; // Gets the current major version. @@ -147,17 +147,17 @@ function updateBetaVersion(done) { } // Rebuild, package and publish to npm. -const publish = gulp.series( - packageTasks.package, // Does clean + build. +export const publish = gulp.series( + packageTasks.pack, // Does clean + build. checkBranch, checkReleaseDir, loginAndPublish ); // Rebuild, package and publish a beta version of Blockly. -const publishBeta = gulp.series( +export const publishBeta = gulp.series( updateBetaVersion, - packageTasks.package, // Does clean + build. + packageTasks.pack, // Does clean + build. checkBranch, checkReleaseDir, loginAndPublishBeta @@ -165,19 +165,10 @@ const publishBeta = gulp.series( // Switch to a new branch, update the version number, build Blockly // and check in the resulting built files. -const recompileDevelop = gulp.series( +export const recompile = gulp.series( gitTasks.syncDevelop(), gitTasks.createRebuildBranch, updateVersionPrompt, - packageTasks.package, // Does clean + build. + packageTasks.pack, // Does clean + build. gitTasks.pushRebuildBranch ); - -module.exports = { - // Main sequence targets. Each should invoke any immediate prerequisite(s). - publishBeta, - publish, - - // Legacy target, to be deleted. - recompile: recompileDevelop, -}; diff --git a/scripts/gulpfiles/test_tasks.js b/packages/blockly/scripts/gulpfiles/test_tasks.mjs similarity index 92% rename from scripts/gulpfiles/test_tasks.js rename to packages/blockly/scripts/gulpfiles/test_tasks.mjs index 236a21d7759..37f9884440d 100644 --- a/scripts/gulpfiles/test_tasks.js +++ b/packages/blockly/scripts/gulpfiles/test_tasks.mjs @@ -9,19 +9,19 @@ */ /* eslint-env node */ -const asyncDone = require('async-done'); -const gulp = require('gulp'); -const gzip = require('gulp-gzip'); -const fs = require('fs'); -const path = require('path'); -const {execSync} = require('child_process'); -const {rimraf} = require('rimraf'); +import asyncDone from 'async-done'; +import * as gulp from 'gulp'; +import gzip from 'gulp-gzip'; +import * as fs from 'fs'; +import * as path from 'path'; +import {execSync} from 'child_process'; +import {rimraf} from 'rimraf'; -const {RELEASE_DIR, TEST_TSC_OUTPUT_DIR} = require('./config'); +import {RELEASE_DIR, TEST_TSC_OUTPUT_DIR} from './config.mjs'; -const {runMochaTestsInBrowser} = require('../../tests/mocha/webdriver.js'); -const {runGeneratorsInBrowser} = require('../../tests/generators/webdriver.js'); -const {runCompileCheckInBrowser} = require('../../tests/compile/webdriver.js'); +import {runMochaTestsInBrowser} from '../../tests/mocha/webdriver.js'; +import {runGeneratorsInBrowser} from '../../tests/generators/webdriver.js'; +import {runCompileCheckInBrowser} from '../../tests/compile/webdriver.js'; const OUTPUT_DIR = 'build/generators'; const GOLDEN_DIR = 'tests/generators/golden'; @@ -257,9 +257,9 @@ async function metadata() { * Run Mocha tests inside a browser. * @return {Promise} Asynchronous result. */ -async function mocha() { +async function mocha(exitOnCompletion = true) { return runTestTask('mocha', async () => { - const result = await runMochaTestsInBrowser().catch(e => { + const result = await runMochaTestsInBrowser(exitOnCompletion).catch(e => { throw e; }); if (result) { @@ -269,6 +269,14 @@ async function mocha() { }); } +/** + * Run Mocha tests inside a browser and keep the browser open upon completion. + * @return {Promise} Asynchronous result. + */ +export async function interactiveMocha() { + return mocha(false); +} + /** * Helper method for comparison file. * @param {string} file1 First target file. @@ -321,7 +329,7 @@ function checkResult(suffix) { * Run generator tests inside a browser and check the results. * @return {Promise} Asynchronous result. */ -async function generators() { +export async function generators() { return runTestTask('generators', async () => { // Clean up. rimraf.sync(OUTPUT_DIR); @@ -396,10 +404,6 @@ const tasks = [ advancedCompileInBrowser ]; -const test = gulp.series(...tasks, reportTestResult); +export const test = gulp.series(...tasks, reportTestResult); -module.exports = { - test, - generators, -}; diff --git a/scripts/helpers.js b/packages/blockly/scripts/helpers.js similarity index 100% rename from scripts/helpers.js rename to packages/blockly/scripts/helpers.js diff --git a/scripts/i18n/common.py b/packages/blockly/scripts/i18n/common.py similarity index 100% rename from scripts/i18n/common.py rename to packages/blockly/scripts/i18n/common.py diff --git a/scripts/i18n/create_messages.py b/packages/blockly/scripts/i18n/create_messages.py similarity index 100% rename from scripts/i18n/create_messages.py rename to packages/blockly/scripts/i18n/create_messages.py diff --git a/scripts/i18n/dedup_json.py b/packages/blockly/scripts/i18n/dedup_json.py similarity index 100% rename from scripts/i18n/dedup_json.py rename to packages/blockly/scripts/i18n/dedup_json.py diff --git a/scripts/i18n/js_to_json.py b/packages/blockly/scripts/i18n/js_to_json.py similarity index 100% rename from scripts/i18n/js_to_json.py rename to packages/blockly/scripts/i18n/js_to_json.py diff --git a/scripts/i18n/tests.py b/packages/blockly/scripts/i18n/tests.py similarity index 100% rename from scripts/i18n/tests.py rename to packages/blockly/scripts/i18n/tests.py diff --git a/scripts/migration/renamings.json5 b/packages/blockly/scripts/migration/renamings.json5 similarity index 100% rename from scripts/migration/renamings.json5 rename to packages/blockly/scripts/migration/renamings.json5 diff --git a/scripts/package/README.md b/packages/blockly/scripts/package/README.md similarity index 100% rename from scripts/package/README.md rename to packages/blockly/scripts/package/README.md diff --git a/scripts/package/core-node.js b/packages/blockly/scripts/package/core-node.js similarity index 100% rename from scripts/package/core-node.js rename to packages/blockly/scripts/package/core-node.js diff --git a/scripts/package/index.js b/packages/blockly/scripts/package/index.js similarity index 100% rename from scripts/package/index.js rename to packages/blockly/scripts/package/index.js diff --git a/scripts/package/templates/umd-msg.template b/packages/blockly/scripts/package/templates/umd-msg.template similarity index 100% rename from scripts/package/templates/umd-msg.template rename to packages/blockly/scripts/package/templates/umd-msg.template diff --git a/scripts/package/templates/umd.template b/packages/blockly/scripts/package/templates/umd.template similarity index 100% rename from scripts/package/templates/umd.template rename to packages/blockly/scripts/package/templates/umd.template diff --git a/scripts/themes/blockStyles_example.json b/packages/blockly/scripts/themes/blockStyles_example.json similarity index 100% rename from scripts/themes/blockStyles_example.json rename to packages/blockly/scripts/themes/blockStyles_example.json diff --git a/scripts/themes/create_blockStyles.py b/packages/blockly/scripts/themes/create_blockStyles.py similarity index 96% rename from scripts/themes/create_blockStyles.py rename to packages/blockly/scripts/themes/create_blockStyles.py index d1f88a4b0db..2b9b2bb162f 100644 --- a/scripts/themes/create_blockStyles.py +++ b/packages/blockly/scripts/themes/create_blockStyles.py @@ -154,12 +154,15 @@ def findRgbVal(colour): # Get info on the input file def getFileInfo(): + from pathlib import Path if (len(sys.argv) < 2): print("Please provide a filename") sys.exit() fileName = sys.argv[1] try: jsonFile = open(fileName).read() + fileName = str(Path(fileName).parent / f"new_{Path(fileName).stem}.json") + fileName = Path(fileName) except IOError as err: print('Could not find that file name') sys.exit() @@ -174,7 +177,7 @@ def createColourMap(): for key in jsonData.keys(): rgbVal = findRgbVal(jsonData[key]) colourObj[key] = findOtherColours(rgbVal) - f= open("new_" + fileName,"w+") + f= open(fileName,"w+") f.write(json.dumps(colourObj, indent=2, sort_keys=True)) f.close() diff --git a/scripts/tsick.js b/packages/blockly/scripts/tsick.js similarity index 100% rename from scripts/tsick.js rename to packages/blockly/scripts/tsick.js diff --git a/tests/browser/.mocharc.js b/packages/blockly/tests/browser/.mocharc.js similarity index 67% rename from tests/browser/.mocharc.js rename to packages/blockly/tests/browser/.mocharc.js index 879bb462378..3c407e688cd 100644 --- a/tests/browser/.mocharc.js +++ b/packages/blockly/tests/browser/.mocharc.js @@ -3,4 +3,5 @@ module.exports = { ui: 'tdd', require: __dirname + '/test/hooks.mjs', + spec: 'tests/browser/test/**/*_test.mjs', }; diff --git a/tests/browser/test/basic_block_factory_test.mjs b/packages/blockly/tests/browser/test/basic_block_factory_test.mjs similarity index 100% rename from tests/browser/test/basic_block_factory_test.mjs rename to packages/blockly/tests/browser/test/basic_block_factory_test.mjs diff --git a/tests/browser/test/basic_block_test.mjs b/packages/blockly/tests/browser/test/basic_block_test.mjs similarity index 92% rename from tests/browser/test/basic_block_test.mjs rename to packages/blockly/tests/browser/test/basic_block_test.mjs index 326e20ecfd4..52912f0cd86 100644 --- a/tests/browser/test/basic_block_test.mjs +++ b/packages/blockly/tests/browser/test/basic_block_test.mjs @@ -31,7 +31,7 @@ suite('Basic block tests', function (done) { test('Drag three blocks into the workspace', async function () { for (let i = 1; i <= 3; i++) { - await dragNthBlockFromFlyout(this.browser, 'Align', 0, 250, 50 * i); + await dragNthBlockFromFlyout(this.browser, 'Align', 0, 50, 50); chai.assert.equal((await getAllBlocks(this.browser)).length, i); } }); diff --git a/packages/blockly/tests/browser/test/basic_playground_test.mjs b/packages/blockly/tests/browser/test/basic_playground_test.mjs new file mode 100644 index 00000000000..72b3894a6a3 --- /dev/null +++ b/packages/blockly/tests/browser/test/basic_playground_test.mjs @@ -0,0 +1,461 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview Node.js script to run Automated tests in Chrome, via webdriver. + */ + +import * as chai from 'chai'; +import { + connect, + contextMenuSelect, + dragBlockTypeFromFlyout, + dragNthBlockFromFlyout, + PAUSE_TIME, + testFileLocations, + testSetup, +} from './test_setup.mjs'; + +async function getIsCollapsed(browser, blockId) { + return await browser.execute((blockId) => { + return Blockly.getMainWorkspace().getBlockById(blockId).isCollapsed(); + }, blockId); +} + +async function getIsDisabled(browser, blockId) { + return await browser.execute((blockId) => { + const block = Blockly.getMainWorkspace().getBlockById(blockId); + return !block.isEnabled() || block.getInheritedDisabled(); + }, blockId); +} + +async function getCommentText(browser, blockId) { + return await browser.execute((blockId) => { + return Blockly.getMainWorkspace().getBlockById(blockId).getCommentText(); + }, blockId); +} + +suite('Testing Connecting Blocks', function () { + // Setting timeout to unlimited as the webdriver takes a longer time to run than most mocha test + this.timeout(0); + + // Setup Selenium for all of the tests + suiteSetup(async function () { + this.browser = await testSetup(testFileLocations.PLAYGROUND); + }); + + test('dragging a block from the flyout results in a block on the workspace', async function () { + await dragBlockTypeFromFlyout(this.browser, 'Logic', 'controls_if', 20, 20); + const blockCount = await this.browser.execute(() => { + return Blockly.getMainWorkspace().getAllBlocks(false).length; + }); + + chai.assert.equal(blockCount, 1); + }); +}); + +/** + * These tests have to run together. Each test acts on the state left by the + * previous test, and each test has a single assertion. + */ +suite('Right Clicking on Blocks', function () { + // Setting timeout to unlimited as the webdriver takes a longer time to run than most mocha test + this.timeout(0); + + // Setup Selenium for all of the tests + suiteSetup(async function () { + this.browser = await testSetup(testFileLocations.PLAYGROUND); + this.block = await dragNthBlockFromFlyout(this.browser, 'Loops', 0, 20, 20); + }); + + test('clicking the collapse option collapses the block', async function () { + await contextMenuSelect(this.browser, this.block, 'Collapse Block'); + chai.assert.isTrue(await getIsCollapsed(this.browser, this.block.id)); + }); + + // Assumes that + test('clicking the expand option expands the block', async function () { + await contextMenuSelect(this.browser, this.block, 'Expand Block'); + chai.assert.isFalse(await getIsCollapsed(this.browser, this.block.id)); + }); + + test('clicking the disable option disables the block', async function () { + await contextMenuSelect(this.browser, this.block, 'Disable Block'); + chai.assert.isTrue(await getIsDisabled(this.browser, this.block.id)); + }); + + test('clicking the enable option enables the block', async function () { + await contextMenuSelect(this.browser, this.block, 'Enable Block'); + chai.assert.isFalse(await getIsDisabled(this.browser, this.block.id)); + }); + + test('clicking the add comment option adds a comment to the block', async function () { + await contextMenuSelect(this.browser, this.block, 'Add Comment'); + chai.assert.equal(await getCommentText(this.browser, this.block.id), ''); + }); + + test('clicking the remove comment option removes a comment from the block', async function () { + await contextMenuSelect(this.browser, this.block, 'Remove Comment'); + chai.assert.isNull(await getCommentText(this.browser, this.block.id)); + }); + + test('does not scroll the page when node is ephemerally focused', async function () { + const initialScroll = await this.browser.execute(() => { + return window.scrollY; + }); + // This left-right-left sequence was necessary to reproduce unintended + // scrolling; regardless of the number of clicks/context menu activations, + // the page should not scroll. + this.block.click({button: 2}); + this.block.click({button: 0}); + this.block.click({button: 2}); + await this.browser.pause(250); + const finalScroll = await this.browser.execute(() => { + return window.scrollY; + }); + + chai.assert.equal(initialScroll, finalScroll); + }); + + test('does not scroll the page when node is actively focused', async function () { + await this.browser.setWindowSize(500, 300); + await this.browser.setViewport({width: 500, height: 300}); + const initialScroll = await this.browser.execute((blockId) => { + window.scrollTo(0, document.body.scrollHeight); + return window.scrollY; + }, this.block.id); + await this.browser.execute(() => { + Blockly.getFocusManager().focusNode( + Blockly.getMainWorkspace().getToolbox(), + ); + }); + const finalScroll = await this.browser.execute(() => { + return window.scrollY; + }); + + chai.assert.equal(initialScroll, finalScroll); + await this.browser.setWindowSize(800, 600); + await this.browser.setViewport({width: 800, height: 600}); + }); +}); + +suite('Disabling', function () { + // Setting timeout to unlimited as the webdriver takes a longer + // time to run than most mocha tests. + this.timeout(0); + + suiteSetup(async function () { + this.browser = await testSetup(testFileLocations.PLAYGROUND); + }); + + setup(async function () { + await this.browser.refresh(); + // Pause to allow refresh time to work. + await this.browser.pause(PAUSE_TIME + 150); + }); + + test( + 'children connected to value inputs are disabled when the ' + + 'parent is disabled', + async function () { + const parent = await dragBlockTypeFromFlyout( + this.browser, + 'Logic', + 'controls_if', + 15, + 0, + ); + const child = await dragBlockTypeFromFlyout( + this.browser, + 'Logic', + 'logic_boolean', + 100, + 0, + ); + await connect(this.browser, child, 'OUTPUT', parent, 'IF0'); + await this.browser.pause(PAUSE_TIME); + await contextMenuSelect(this.browser, parent, 'Disable Block'); + + chai.assert.isTrue(await getIsDisabled(this.browser, child.id)); + }, + ); + + test( + 'children connected to statement inputs are disabled when the ' + + 'parent is disabled', + async function () { + const parent = await dragBlockTypeFromFlyout( + this.browser, + 'Logic', + 'controls_if', + 15, + 0, + ); + const child = await dragBlockTypeFromFlyout( + this.browser, + 'Logic', + 'controls_if', + 100, + 0, + ); + await this.browser.pause(PAUSE_TIME); + await connect(this.browser, child, 'PREVIOUS', parent, 'DO0'); + + await this.browser.pause(PAUSE_TIME); + await contextMenuSelect(this.browser, parent, 'Disable Block'); + + chai.assert.isTrue(await getIsDisabled(this.browser, child.id)); + }, + ); + + test( + 'children connected to next connections are not disabled when the ' + + 'parent is disabled', + async function () { + const parent = await dragBlockTypeFromFlyout( + this.browser, + 'Logic', + 'controls_if', + 15, + 0, + ); + const child = await dragBlockTypeFromFlyout( + this.browser, + 'Logic', + 'controls_if', + 100, + 0, + ); + + await connect(this.browser, child, 'PREVIOUS', parent, 'NEXT'); + + await contextMenuSelect(this.browser, parent, 'Disable Block'); + + chai.assert.isFalse(await getIsDisabled(this.browser, child.id)); + }, + ); +}); + +suite('Focused nodes are scrolled into bounds', function () { + // Setting timeout to unlimited as the webdriver takes a longer time to run + // than most mocha tests. + this.timeout(0); + + // Setup Selenium for all of the tests + suiteSetup(async function () { + this.browser = await testSetup(testFileLocations.PLAYGROUND); + await this.browser.execute(() => { + window.focusScrollTest = async (testcase) => { + const workspace = Blockly.getMainWorkspace(); + const metrics = workspace.getMetricsManager(); + const initialViewport = metrics.getViewMetrics(true); + const elementBounds = await testcase(workspace); + await Blockly.renderManagement.finishQueuedRenders(); + const scrolledViewport = metrics.getViewMetrics(true); + const workspaceBounds = new Blockly.utils.Rect( + scrolledViewport.top, + scrolledViewport.top + scrolledViewport.height, + scrolledViewport.left, + scrolledViewport.left + scrolledViewport.width, + ); + return { + changed: + JSON.stringify(initialViewport) !== + JSON.stringify(scrolledViewport), + intersects: elementBounds.intersects(workspaceBounds), + contains: workspaceBounds.contains( + elementBounds.getOrigin().x, + elementBounds.getOrigin().y, + ), + elementBounds, + workspaceBounds, + }; + }; + }); + }); + + setup(async function () { + await this.browser.execute(() => { + Blockly.serialization.blocks.append( + { + 'type': 'text', + 'x': -500, + 'y': -500, + }, + Blockly.getMainWorkspace(), + ); + Blockly.serialization.blocks.append( + { + 'type': 'controls_if', + 'x': 500, + 'y': 500, + }, + Blockly.getMainWorkspace(), + ); + Blockly.getMainWorkspace().zoomCenter(1); + }); + }); + + test('Focused blocks scroll into bounds', async function () { + const result = await this.browser.execute(async () => { + return await window.focusScrollTest(async (workspace) => { + const block = workspace.getTopBlocks()[0]; + Blockly.getFocusManager().focusNode(block); + return block.getBoundingRectangleWithoutChildren(); + }); + }); + chai.assert.isTrue(result.intersects); + chai.assert.isTrue(result.contains); + chai.assert.isTrue(result.changed); + }); + + test('Focused bubbles scroll into bounds', async function () { + const result = await this.browser.execute(async () => { + return await window.focusScrollTest(async (workspace) => { + const block = workspace.getTopBlocks()[0]; + block.setCommentText('hello world'); + const icon = block.getIcon(Blockly.icons.IconType.COMMENT); + icon.setBubbleVisible(true); + await Blockly.renderManagement.finishQueuedRenders(); + icon.setBubbleLocation(new Blockly.utils.Coordinate(-510, -510)); + Blockly.getFocusManager().focusNode(icon.getBubble()); + const xy = icon.getBubble().getRelativeToSurfaceXY(); + const size = icon.getBubble().getSize(); + return new Blockly.utils.Rect( + xy.y, + xy.y + size.height, + xy.x, + xy.x + size.width, + ); + }); + }); + + chai.assert.isTrue(result.intersects); + chai.assert.isTrue(result.contains); + chai.assert.isTrue(result.changed); + }); + + test('Comment bar buttons scroll into bounds', async function () { + const result = await this.browser.execute(async () => { + return await window.focusScrollTest(async (workspace) => { + const comment = new Blockly.comments.RenderedWorkspaceComment( + workspace, + ); + comment.moveTo(new Blockly.utils.Coordinate(-300, 500)); + const commentBarButton = comment.view.getCommentBarButtons()[0]; + Blockly.getFocusManager().focusNode(commentBarButton); + const xy = comment.view.getRelativeToSurfaceXY(); + const size = comment.view.getSize(); + // Comment bar buttons scroll their parent comment view into view. + return new Blockly.utils.Rect( + xy.y, + xy.y + size.height, + xy.x, + xy.x + size.width, + ); + }); + }); + + chai.assert.isTrue(result.intersects); + chai.assert.isTrue(result.contains); + chai.assert.isTrue(result.changed); + }); + + test('Comment editors scroll into bounds', async function () { + const result = await this.browser.execute(async () => { + return await window.focusScrollTest(async (workspace) => { + const comment = new Blockly.comments.RenderedWorkspaceComment( + workspace, + ); + comment.moveTo(new Blockly.utils.Coordinate(-300, 500)); + const commentEditor = comment.view.getEditorFocusableNode(); + Blockly.getFocusManager().focusNode(commentEditor); + // Comment editor bounds can't be calculated externally since they + // depend on private properties, but the comment view is a reasonable + // proxy. + const xy = comment.view.getRelativeToSurfaceXY(); + const size = comment.view.getSize(); + return new Blockly.utils.Rect( + xy.y, + xy.y + size.height, + xy.x, + xy.x + size.width, + ); + }); + }); + + chai.assert.isTrue(result.intersects); + chai.assert.isTrue(result.contains); + chai.assert.isTrue(result.changed); + }); + + test('Workspace comments scroll into bounds', async function () { + const result = await this.browser.execute(async () => { + return await window.focusScrollTest(async (workspace) => { + const comment = new Blockly.comments.RenderedWorkspaceComment( + workspace, + ); + comment.moveTo(new Blockly.utils.Coordinate(-500, 500)); + Blockly.getFocusManager().focusNode(comment); + return comment.getBoundingRectangle(); + }); + }); + + chai.assert.isTrue(result.intersects); + chai.assert.isTrue(result.contains); + chai.assert.isTrue(result.changed); + }); + + test('Icons scroll into bounds', async function () { + const result = await this.browser.execute(async () => { + return await window.focusScrollTest(async (workspace) => { + const block = workspace.getTopBlocks()[0]; + block.setWarningText('this is bad'); + const icon = block.getIcon(Blockly.icons.IconType.WARNING); + Blockly.getFocusManager().focusNode(icon); + // Icon bounds can't be calculated externally since they depend on + // protected properties, but the parent block is a reasonable proxy. + return block.getBoundingRectangleWithoutChildren(); + }); + }); + + chai.assert.isTrue(result.intersects); + chai.assert.isTrue(result.contains); + chai.assert.isTrue(result.changed); + }); + + test('Fields scroll into bounds', async function () { + const result = await this.browser.execute(async () => { + return await window.focusScrollTest(async (workspace) => { + const block = workspace.getTopBlocks()[0]; + const field = block.getField('TEXT'); + Blockly.getFocusManager().focusNode(field); + // Fields scroll their source block into view. + return block.getBoundingRectangleWithoutChildren(); + }); + }); + + chai.assert.isTrue(result.intersects); + chai.assert.isTrue(result.contains); + chai.assert.isTrue(result.changed); + }); + + test('Connections scroll into bounds', async function () { + const result = await this.browser.execute(async () => { + return await window.focusScrollTest(async (workspace) => { + const block = workspace.getBlocksByType('controls_if')[0]; + Blockly.getFocusManager().focusNode(block.nextConnection); + // Connection bounds can't be calculated externally since they depend on + // protected properties, but the parent block is a reasonable proxy. + return block.getBoundingRectangleWithoutChildren(); + }); + }); + + chai.assert.isTrue(result.intersects); + chai.assert.isTrue(result.contains); + chai.assert.isTrue(result.changed); + }); +}); diff --git a/tests/browser/test/block_undo_test.mjs b/packages/blockly/tests/browser/test/block_undo_test.mjs similarity index 100% rename from tests/browser/test/block_undo_test.mjs rename to packages/blockly/tests/browser/test/block_undo_test.mjs diff --git a/packages/blockly/tests/browser/test/clipboard_test.mjs b/packages/blockly/tests/browser/test/clipboard_test.mjs new file mode 100644 index 00000000000..37dd359d3b9 --- /dev/null +++ b/packages/blockly/tests/browser/test/clipboard_test.mjs @@ -0,0 +1,611 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as chai from 'chai'; +import {Key} from 'webdriverio'; +import { + PAUSE_TIME, + clickWorkspace, + focusOnBlock, + getAllBlocks, + getBlockTypeFromWorkspace, + getCategory, + getSelectedBlockId, + getSelectedBlockType, + openMutatorForBlock, + testFileLocations, + testSetup, +} from './test_setup.mjs'; + +const testBlockJson = { + 'blocks': { + 'languageVersion': 0, + 'blocks': [ + { + 'type': 'controls_repeat_ext', + 'id': 'controls_repeat_1', + 'x': 88, + 'y': 88, + 'inputs': { + 'TIMES': { + 'shadow': { + 'type': 'math_number', + 'id': 'math_number_shadow_1', + 'fields': { + 'NUM': 10, + }, + }, + }, + 'DO': { + 'block': { + 'type': 'controls_if', + 'id': 'controls_if_1', + 'inputs': { + 'IF0': { + 'block': { + 'type': 'logic_boolean', + 'id': 'logic_boolean_1', + 'fields': { + 'BOOL': 'TRUE', + }, + }, + }, + 'DO0': { + 'block': { + 'type': 'text_print', + 'id': 'text_print_1', + 'inputs': { + 'TEXT': { + 'shadow': { + 'type': 'text', + 'id': 'text_shadow_1', + 'fields': { + 'TEXT': 'abc', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + ], + }, +}; + +async function loadStartBlocks(browser) { + await browser.execute((stringifiedJson) => { + // Hangs forever if the json isn't stringified ¯\_(ツ)_/¯ + const testBlockJson = JSON.parse(stringifiedJson); + const workspace = Blockly.common.getMainWorkspace(); + Blockly.serialization.workspaces.load(testBlockJson, workspace); + }, JSON.stringify(testBlockJson)); + await browser.pause(PAUSE_TIME); +} + +suite('Clipboard test', async function () { + // Setting timeout to unlimited as these tests take longer time to run + this.timeout(0); + + // Clear the workspace and load start blocks + setup(async function () { + this.browser = await testSetup(testFileLocations.PLAYGROUND); + await this.browser.pause(PAUSE_TIME); + }); + + test('Paste block to/from main workspace', async function () { + await loadStartBlocks(this.browser); + // Select and copy the "true" block + await focusOnBlock(this.browser, 'logic_boolean_1'); + await this.browser.pause(PAUSE_TIME); + + await this.browser.keys([Key.Ctrl, 'c']); + await this.browser.pause(PAUSE_TIME); + + // Check how many blocks there are before pasting + const allBlocksBeforePaste = await getAllBlocks(this.browser); + + // Paste the block while still in the main workspace + await this.browser.keys([Key.Ctrl, 'v']); + await this.browser.pause(PAUSE_TIME); + + // Check result + const allBlocksAfterPaste = await getAllBlocks(this.browser); + chai.assert.equal( + allBlocksAfterPaste.length, + allBlocksBeforePaste.length + 1, + 'Expected there to be one additional block after paste', + ); + const focusedBlockId = await getSelectedBlockId(this.browser); + chai.assert.notEqual( + focusedBlockId, + 'logic_boolean_1', + 'Newly pasted block should be selected', + ); + const focusedBlockType = await getSelectedBlockType(this.browser); + chai.assert.equal( + focusedBlockType, + 'logic_boolean', + 'Newly pasted block should be selected', + ); + }); + + test('Copying a block also copies and pastes its children', async function () { + await loadStartBlocks(this.browser); + // Select and copy the "if/else" block which has children + await focusOnBlock(this.browser, 'controls_if_1'); + await this.browser.pause(PAUSE_TIME); + + await this.browser.keys([Key.Ctrl, 'c']); + await this.browser.pause(PAUSE_TIME); + + // Check how many blocks there are before pasting + const allBlocksBeforePaste = await getAllBlocks(this.browser); + + // Paste the block while still in the main workspace + await this.browser.keys([Key.Ctrl, 'v']); + await this.browser.pause(PAUSE_TIME); + + // Check result + const allBlocksAfterPaste = await getAllBlocks(this.browser); + chai.assert.equal( + allBlocksAfterPaste.length, + allBlocksBeforePaste.length + 4, + 'Expected there to be four additional blocks after paste', + ); + }); + + test('Paste shadow block to/from main workspace', async function () { + await loadStartBlocks(this.browser); + // Select and copy the shadow number block + await focusOnBlock(this.browser, 'math_number_shadow_1'); + await this.browser.pause(PAUSE_TIME); + + await this.browser.keys([Key.Ctrl, 'c']); + await this.browser.pause(PAUSE_TIME); + + // Check how many blocks there are before pasting + const allBlocksBeforePaste = await getAllBlocks(this.browser); + + // Paste the block while still in the main workspace + await this.browser.keys([Key.Ctrl, 'v']); + await this.browser.pause(PAUSE_TIME); + + // Check result + const allBlocksAfterPaste = await getAllBlocks(this.browser); + chai.assert.equal( + allBlocksAfterPaste.length, + allBlocksBeforePaste.length + 1, + 'Expected there to be one additional block after paste', + ); + const focusedBlockId = await getSelectedBlockId(this.browser); + chai.assert.notEqual( + focusedBlockId, + 'math_number_shadow_1', + 'Newly pasted block should be selected', + ); + const focusedBlockType = await getSelectedBlockType(this.browser); + chai.assert.equal( + focusedBlockType, + 'math_number', + 'Newly pasted block should be selected', + ); + const focusedBlockIsShadow = await this.browser.execute(() => { + return Blockly.common.getSelected().isShadow(); + }); + chai.assert.isFalse( + focusedBlockIsShadow, + 'Expected the pasted version of the block to not be a shadow block', + ); + }); + + test('Copy block from flyout, paste to main workspace', async function () { + // Open flyout + await getCategory(this.browser, 'Logic').then((category) => + category.click(), + ); + + // Focus on first block in flyout + await this.browser.execute(() => { + const ws = Blockly.getMainWorkspace().getFlyout().getWorkspace(); + const block = ws.getBlocksByType('controls_if')[0]; + Blockly.getFocusManager().focusNode(block); + }); + await this.browser.pause(PAUSE_TIME); + + // Copy + await this.browser.keys([Key.Ctrl, 'c']); + await this.browser.pause(PAUSE_TIME); + + // Select the main workspace + await clickWorkspace(this.browser); + await this.browser.pause(PAUSE_TIME); + + // Paste + await this.browser.keys([Key.Ctrl, 'v']); + await this.browser.pause(PAUSE_TIME); + + // Check that the block is now on the workspace and selected + const allBlocks = await getAllBlocks(this.browser); + chai.assert.equal( + allBlocks.length, + 1, + 'Expected there to be one block on main workspace after paste from flyout', + ); + + const focusedBlockType = await getSelectedBlockType(this.browser); + chai.assert.equal( + focusedBlockType, + 'controls_if', + 'Newly pasted block should be selected', + ); + }); + + test('Copy block from flyout, paste while flyout focused', async function () { + // Open flyout + await getCategory(this.browser, 'Logic').then((category) => + category.click(), + ); + + // Focus on first block in flyout + await this.browser.execute(() => { + const ws = Blockly.getMainWorkspace().getFlyout().getWorkspace(); + const block = ws.getBlocksByType('controls_if')[0]; + Blockly.getFocusManager().focusNode(block); + }); + await this.browser.pause(PAUSE_TIME); + + // Copy + await this.browser.keys([Key.Ctrl, 'c']); + await this.browser.pause(PAUSE_TIME); + + // Paste + await this.browser.keys([Key.Ctrl, 'v']); + await this.browser.pause(PAUSE_TIME); + + // Check that the flyout is closed + const flyoutIsVisible = await this.browser + .$('.blocklyToolboxFlyout') + .then((elem) => elem.isDisplayed()); + chai.assert.isFalse(flyoutIsVisible, 'Expected flyout to not be open'); + + // Check that the block is now on the main workspace and selected + const allBlocks = await getAllBlocks(this.browser); + chai.assert.equal( + allBlocks.length, + 1, + 'Expected there to be one block on main workspace after paste from flyout', + ); + + const focusedBlockType = await getSelectedBlockType(this.browser); + chai.assert.equal( + focusedBlockType, + 'controls_if', + 'Newly pasted block should be selected', + ); + }); + + test('Copy block from mutator flyout, paste to mutator workspace', async function () { + // Load the start blocks + await loadStartBlocks(this.browser); + + // Open the controls_if mutator + const block = await getBlockTypeFromWorkspace( + this.browser, + 'controls_if', + 0, + ); + await openMutatorForBlock(this.browser, block); + + // Select the first block in the mutator flyout + await this.browser.execute( + (blockId, mutatorBlockType) => { + const flyoutBlock = Blockly.getMainWorkspace() + .getBlockById(blockId) + .mutator.getWorkspace() + .getFlyout() + .getWorkspace() + .getBlocksByType(mutatorBlockType)[0]; + + Blockly.getFocusManager().focusNode(flyoutBlock); + }, + 'controls_if_1', + 'controls_if_elseif', + ); + await this.browser.pause(PAUSE_TIME); + + // Copy + await this.browser.keys([Key.Ctrl, 'c']); + await this.browser.pause(PAUSE_TIME); + + // Paste + await this.browser.keys([Key.Ctrl, 'v']); + await this.browser.pause(PAUSE_TIME); + + // Check that the block is now in the mutator workspace and selected + const numberOfIfElseBlocks = await this.browser.execute( + (blockId, mutatorBlockType) => { + return Blockly.getMainWorkspace() + .getBlockById(blockId) + .mutator.getWorkspace() + .getBlocksByType(mutatorBlockType).length; + }, + 'controls_if_1', + 'controls_if_elseif', + ); + + chai.assert.equal( + numberOfIfElseBlocks, + 1, + 'Expected there to be one if_else block in mutator workspace', + ); + + const focusedBlockType = await getSelectedBlockType(this.browser); + chai.assert.equal( + focusedBlockType, + 'controls_if_elseif', + 'Newly pasted block should be selected', + ); + }); + + test('Copy block from mutator flyout, paste to main workspace while mutator open', async function () { + // Load the start blocks + await loadStartBlocks(this.browser); + + // Open the controls_if mutator + const block = await getBlockTypeFromWorkspace( + this.browser, + 'controls_if', + 0, + ); + await openMutatorForBlock(this.browser, block); + + // Select the first block in the mutator flyout + await this.browser.execute( + (blockId, mutatorBlockType) => { + const flyoutBlock = Blockly.getMainWorkspace() + .getBlockById(blockId) + .mutator.getWorkspace() + .getFlyout() + .getWorkspace() + .getBlocksByType(mutatorBlockType)[0]; + + Blockly.getFocusManager().focusNode(flyoutBlock); + }, + 'controls_if_1', + 'controls_if_elseif', + ); + await this.browser.pause(PAUSE_TIME); + + // Copy + await this.browser.keys([Key.Ctrl, 'c']); + await this.browser.pause(PAUSE_TIME); + + // Click the main workspace + await clickWorkspace(this.browser); + + // Paste + await this.browser.keys([Key.Ctrl, 'v']); + await this.browser.pause(PAUSE_TIME); + + // Check that the block is now in the mutator workspace and selected + const numberOfIfElseBlocks = await this.browser.execute( + (blockId, mutatorBlockType) => { + return Blockly.getMainWorkspace() + .getBlockById(blockId) + .mutator.getWorkspace() + .getBlocksByType(mutatorBlockType).length; + }, + 'controls_if_1', + 'controls_if_elseif', + ); + + chai.assert.equal( + numberOfIfElseBlocks, + 1, + 'Expected there to be one if_else block in mutator workspace', + ); + + const focusedBlockType = await getSelectedBlockType(this.browser); + chai.assert.equal( + focusedBlockType, + 'controls_if_elseif', + 'Newly pasted block should be selected', + ); + + // Check that there are no new blocks on the main workspace + const numberOfIfElseBlocksOnMainWorkspace = await this.browser.execute( + (mutatorBlockType) => { + return Blockly.getMainWorkspace().getBlocksByType(mutatorBlockType) + .length; + }, + 'controls_if_elseif', + ); + chai.assert.equal( + numberOfIfElseBlocksOnMainWorkspace, + 0, + 'Mutator blocks should not appear on main workspace', + ); + }); + + test('Copy block from mutator flyout, paste to main workspace while mutator closed', async function () { + // Load the start blocks + await loadStartBlocks(this.browser); + + // Open the controls_if mutator + const block = await getBlockTypeFromWorkspace( + this.browser, + 'controls_if', + 0, + ); + await openMutatorForBlock(this.browser, block); + + // Select the first block in the mutator flyout + await this.browser.execute( + (blockId, mutatorBlockType) => { + const flyoutBlock = Blockly.getMainWorkspace() + .getBlockById(blockId) + .mutator.getWorkspace() + .getFlyout() + .getWorkspace() + .getBlocksByType(mutatorBlockType)[0]; + + Blockly.getFocusManager().focusNode(flyoutBlock); + }, + 'controls_if_1', + 'controls_if_elseif', + ); + await this.browser.pause(PAUSE_TIME); + + // Copy + await this.browser.keys([Key.Ctrl, 'c']); + await this.browser.pause(PAUSE_TIME); + + // Close the mutator flyout (calling this method on open mutator closes it) + await openMutatorForBlock(this.browser, block); + + // Click the main workspace + await clickWorkspace(this.browser); + + // Paste + await this.browser.keys([Key.Ctrl, 'v']); + await this.browser.pause(PAUSE_TIME); + + // Check that there are no new blocks on the main workspace + const numberOfIfElseBlocksOnMainWorkspace = await this.browser.execute( + (mutatorBlockType) => { + return Blockly.getMainWorkspace().getBlocksByType(mutatorBlockType) + .length; + }, + 'controls_if_elseif', + ); + chai.assert.equal( + numberOfIfElseBlocksOnMainWorkspace, + 0, + 'Mutator blocks should not appear on main workspace', + ); + }); + + test('Copy workspace comment, paste to main workspace', async function () { + // Add a workspace comment to the workspace + await this.browser.execute(() => { + const workspace = Blockly.getMainWorkspace(); + const json = { + 'workspaceComments': [ + { + 'height': 100, + 'width': 120, + 'id': 'workspace_comment_1', + 'x': 13, + 'y': -12, + 'text': 'This is a comment', + }, + ], + }; + Blockly.serialization.workspaces.load(json, workspace); + }); + await this.browser.pause(PAUSE_TIME); + + // Select the workspace comment + await this.browser.execute(() => { + const comment = Blockly.getMainWorkspace().getCommentById( + 'workspace_comment_1', + ); + Blockly.getFocusManager().focusNode(comment); + }); + await this.browser.pause(PAUSE_TIME); + + // Copy + await this.browser.keys([Key.Ctrl, 'c']); + await this.browser.pause(PAUSE_TIME); + + // Click the main workspace + await clickWorkspace(this.browser); + + // Paste + await this.browser.keys([Key.Ctrl, 'v']); + await this.browser.pause(PAUSE_TIME); + + // Check that there are 2 comments on the workspace + const numberOfComments = await this.browser.execute(() => { + return Blockly.getMainWorkspace().getTopComments().length; + }); + chai.assert.equal( + numberOfComments, + 2, + 'Expected 2 workspace comments after pasting', + ); + }); + + test('Cut block from main workspace, paste to main workspace', async function () { + await loadStartBlocks(this.browser); + // Select and cut the "true" block + await focusOnBlock(this.browser, 'logic_boolean_1'); + await this.browser.pause(PAUSE_TIME); + + await this.browser.keys([Key.Ctrl, 'x']); + await this.browser.pause(PAUSE_TIME); + + // Check that the "true" block was deleted + const trueBlock = await this.browser.execute(() => { + return Blockly.getMainWorkspace().getBlockById('logic_boolean_1') ?? null; + }); + chai.assert.isNull(trueBlock); + + // Check how many blocks there are before pasting + const allBlocksBeforePaste = await getAllBlocks(this.browser); + + // Paste the block while still in the main workspace + await this.browser.keys([Key.Ctrl, 'v']); + await this.browser.pause(PAUSE_TIME); + + // Check result + const allBlocksAfterPaste = await getAllBlocks(this.browser); + chai.assert.equal( + allBlocksAfterPaste.length, + allBlocksBeforePaste.length + 1, + 'Expected there to be one additional block after paste', + ); + }); + + test('Cannot cut block from flyout', async function () { + // Open flyout + await getCategory(this.browser, 'Logic').then((category) => + category.click(), + ); + + // Focus on first block in flyout + await this.browser.execute(() => { + const ws = Blockly.getMainWorkspace().getFlyout().getWorkspace(); + const block = ws.getBlocksByType('controls_if')[0]; + Blockly.getFocusManager().focusNode(block); + }); + await this.browser.pause(PAUSE_TIME); + + // Cut + await this.browser.keys([Key.Ctrl, 'x']); + await this.browser.pause(PAUSE_TIME); + + // Select the main workspace + await clickWorkspace(this.browser); + await this.browser.pause(PAUSE_TIME); + + // Paste + await this.browser.keys([Key.Ctrl, 'v']); + await this.browser.pause(PAUSE_TIME); + + // Check that no block was pasted + const allBlocks = await getAllBlocks(this.browser); + chai.assert.equal( + allBlocks.length, + 0, + 'Expected no blocks in the workspace because nothing to paste', + ); + }); +}); diff --git a/tests/browser/test/delete_blocks_test.mjs b/packages/blockly/tests/browser/test/delete_blocks_test.mjs similarity index 95% rename from tests/browser/test/delete_blocks_test.mjs rename to packages/blockly/tests/browser/test/delete_blocks_test.mjs index a5df88705c5..5c2499c41f3 100644 --- a/tests/browser/test/delete_blocks_test.mjs +++ b/packages/blockly/tests/browser/test/delete_blocks_test.mjs @@ -8,6 +8,7 @@ import * as chai from 'chai'; import {Key} from 'webdriverio'; import { clickBlock, + clickWorkspace, contextMenuSelect, getAllBlocks, getBlockElementById, @@ -141,7 +142,7 @@ suite('Delete blocks', function (done) { test('Delete block using backspace key', async function () { const before = (await getAllBlocks(this.browser)).length; // Get first print block, click to select it, and delete it using backspace key. - await clickBlock(this.browser, this.firstBlock, {button: 1}); + await clickBlock(this.browser, this.firstBlock.id, {button: 1}); await this.browser.keys([Key.Backspace]); const after = (await getAllBlocks(this.browser)).length; chai.assert.equal( @@ -154,7 +155,7 @@ suite('Delete blocks', function (done) { test('Delete block using delete key', async function () { const before = (await getAllBlocks(this.browser)).length; // Get first print block, click to select it, and delete it using delete key. - await clickBlock(this.browser, this.firstBlock, {button: 1}); + await clickBlock(this.browser, this.firstBlock.id, {button: 1}); await this.browser.keys([Key.Delete]); const after = (await getAllBlocks(this.browser)).length; chai.assert.equal( @@ -179,7 +180,7 @@ suite('Delete blocks', function (done) { test('Undo block deletion', async function () { const before = (await getAllBlocks(this.browser)).length; // Get first print block, click to select it, and delete it using backspace key. - await clickBlock(this.browser, this.firstBlock, {button: 1}); + await clickBlock(this.browser, this.firstBlock.id, {button: 1}); await this.browser.keys([Key.Backspace]); await this.browser.pause(PAUSE_TIME); // Undo @@ -187,8 +188,8 @@ suite('Delete blocks', function (done) { await this.browser.pause(PAUSE_TIME); const after = (await getAllBlocks(this.browser)).length; chai.assert.equal( - before, after, + before, 'Expected there to be the original number of blocks after undoing a delete', ); }); @@ -196,13 +197,14 @@ suite('Delete blocks', function (done) { test('Redo block deletion', async function () { const before = (await getAllBlocks(this.browser)).length; // Get first print block, click to select it, and delete it using backspace key. - await clickBlock(this.browser, this.firstBlock, {button: 1}); + await clickBlock(this.browser, this.firstBlock.id, {button: 1}); await this.browser.keys([Key.Backspace]); await this.browser.pause(PAUSE_TIME); // Undo await this.browser.keys([Key.Ctrl, 'z']); await this.browser.pause(PAUSE_TIME); // Redo + await clickWorkspace(this.browser); await this.browser.keys([Key.Ctrl, Key.Shift, 'z']); await this.browser.pause(PAUSE_TIME); const after = (await getAllBlocks(this.browser)).length; diff --git a/tests/browser/test/extensive_test.mjs b/packages/blockly/tests/browser/test/extensive_test.mjs similarity index 97% rename from tests/browser/test/extensive_test.mjs rename to packages/blockly/tests/browser/test/extensive_test.mjs index 786be0ade53..48c066c399d 100644 --- a/tests/browser/test/extensive_test.mjs +++ b/packages/blockly/tests/browser/test/extensive_test.mjs @@ -11,8 +11,8 @@ import * as chai from 'chai'; import {Key} from 'webdriverio'; import { + clickBlock, getAllBlocks, - getBlockElementById, PAUSE_TIME, testFileLocations, testSetup, @@ -33,11 +33,7 @@ suite('This tests loading Large Configuration and Deletion', function (done) { }); test('deleting block results in the correct number of blocks', async function () { - const fourthRepeatDo = await getBlockElementById( - this.browser, - 'E8bF[-r:B~cabGLP#QYd', - ); - await fourthRepeatDo.click({x: -100, y: -40}); + await clickBlock(this.browser, 'E8bF[-r:B~cabGLP#QYd', {button: 1}); await this.browser.keys([Key.Delete]); await this.browser.pause(PAUSE_TIME); const allBlocks = await getAllBlocks(this.browser); diff --git a/tests/browser/test/field_edits_test.mjs b/packages/blockly/tests/browser/test/field_edits_test.mjs similarity index 100% rename from tests/browser/test/field_edits_test.mjs rename to packages/blockly/tests/browser/test/field_edits_test.mjs diff --git a/tests/browser/test/hooks.mjs b/packages/blockly/tests/browser/test/hooks.mjs similarity index 100% rename from tests/browser/test/hooks.mjs rename to packages/blockly/tests/browser/test/hooks.mjs diff --git a/tests/browser/test/mutator_test.mjs b/packages/blockly/tests/browser/test/mutator_test.mjs similarity index 95% rename from tests/browser/test/mutator_test.mjs rename to packages/blockly/tests/browser/test/mutator_test.mjs index 6d077b9fd92..b12ae5698c9 100644 --- a/tests/browser/test/mutator_test.mjs +++ b/packages/blockly/tests/browser/test/mutator_test.mjs @@ -34,16 +34,15 @@ async function testMutator(browser, delta) { browser, 'Logic', 'controls_if', - delta * 50, + delta * 150, 50, ); await openMutatorForBlock(browser, mutatorBlock); - await browser.pause(PAUSE_TIME); await dragBlockFromMutatorFlyout( browser, mutatorBlock, 'controls_if_elseif', - delta * 50, + delta * 150, 50, ); await browser.pause(PAUSE_TIME); @@ -67,8 +66,8 @@ async function testMutator(browser, delta) { 'g:nth-child(2) > svg:nth-child(1) > g > g.blocklyBlockCanvas > ' + 'g.blocklyDraggable', ); - // For some reason this needs a lot more time. - await browser.pause(2000); + + await browser.pause(PAUSE_TIME); await connect( browser, await getBlockElementById(browser, elseIfQuarkId), diff --git a/tests/browser/test/procedure_test.mjs b/packages/blockly/tests/browser/test/procedure_test.mjs similarity index 63% rename from tests/browser/test/procedure_test.mjs rename to packages/blockly/tests/browser/test/procedure_test.mjs index c01eb49561c..d1990fddc4a 100644 --- a/tests/browser/test/procedure_test.mjs +++ b/packages/blockly/tests/browser/test/procedure_test.mjs @@ -11,9 +11,8 @@ import * as chai from 'chai'; import { connect, - getBlockTypeFromCategory, - getNthBlockOfCategory, - getSelectedBlockElement, + dragBlockTypeFromFlyout, + dragNthBlockFromFlyout, PAUSE_TIME, testFileLocations, testSetup, @@ -33,43 +32,41 @@ suite('Testing Connecting Blocks', function (done) { test('Testing Procedure', async function () { // Drag out first function - let proceduresDefReturn = await getBlockTypeFromCategory( + const doSomething = await dragBlockTypeFromFlyout( this.browser, 'Functions', 'procedures_defreturn', + 50, + 20, ); - await proceduresDefReturn.dragAndDrop({x: 50, y: 20}); - const doSomething = await getSelectedBlockElement(this.browser); - // Drag out second function. - proceduresDefReturn = await getBlockTypeFromCategory( + const doSomething2 = await dragBlockTypeFromFlyout( this.browser, 'Functions', 'procedures_defreturn', + 50, + 20, ); - await proceduresDefReturn.dragAndDrop({x: 300, y: 200}); - const doSomething2 = await getSelectedBlockElement(this.browser); - // Drag out numeric - const mathNumeric = await getBlockTypeFromCategory( + const numeric = await dragBlockTypeFromFlyout( this.browser, 'Math', 'math_number', + 50, + 20, ); - await mathNumeric.dragAndDrop({x: 50, y: 20}); - const numeric = await getSelectedBlockElement(this.browser); // Connect numeric to first procedure await connect(this.browser, numeric, 'OUTPUT', doSomething, 'RETURN'); // Drag out doSomething caller from flyout. - const doSomethingFlyout = await getNthBlockOfCategory( + const doSomethingCaller = await dragNthBlockFromFlyout( this.browser, 'Functions', 3, + 50, + 20, ); - await doSomethingFlyout.dragAndDrop({x: 50, y: 20}); - const doSomethingCaller = await getSelectedBlockElement(this.browser); // Connect the doSomething caller to doSomething2 await connect( @@ -81,22 +78,22 @@ suite('Testing Connecting Blocks', function (done) { ); // Drag out print from flyout. - const printFlyout = await getBlockTypeFromCategory( + const print = await dragBlockTypeFromFlyout( this.browser, 'Text', 'text_print', + 50, + 0, ); - await printFlyout.dragAndDrop({x: 50, y: 20}); - const print = await getSelectedBlockElement(this.browser); // Drag out doSomething2 caller from flyout. - const doSomething2Flyout = await getNthBlockOfCategory( + const doSomething2Caller = await dragNthBlockFromFlyout( this.browser, 'Functions', 4, + 50, + 20, ); - await doSomething2Flyout.dragAndDrop({x: 130, y: 20}); - const doSomething2Caller = await getSelectedBlockElement(this.browser); // Connect doSomething2 caller with print. await connect(this.browser, doSomething2Caller, 'OUTPUT', print, 'TEXT'); diff --git a/tests/browser/test/test_setup.mjs b/packages/blockly/tests/browser/test/test_setup.mjs similarity index 79% rename from tests/browser/test/test_setup.mjs rename to packages/blockly/tests/browser/test/test_setup.mjs index 9b48a3638ae..0a8998c3efe 100644 --- a/tests/browser/test/test_setup.mjs +++ b/packages/blockly/tests/browser/test/test_setup.mjs @@ -62,6 +62,8 @@ export async function driverSetup() { // Use Selenium to bring up the page console.log('Starting webdriverio...'); driver = await webdriverio.remote(options); + driver.setWindowSize(800, 600); + driver.setViewport({width: 800, height: 600}); return driver; } @@ -125,6 +127,23 @@ export const screenDirection = { LTR: 1, }; +/** + * Focuses and selects a block with the provided ID. + * + * This throws an error if no block exists for the specified ID. + * + * @param browser The active WebdriverIO Browser object. + * @param blockId The ID of the block to select. + */ +export async function focusOnBlock(browser, blockId) { + return await browser.execute((blockId) => { + const workspaceSvg = Blockly.getMainWorkspace(); + const block = workspaceSvg.getBlockById(blockId); + if (!block) throw new Error(`No block found with ID: ${blockId}.`); + Blockly.getFocusManager().focusNode(block); + }, blockId); +} + /** * @param browser The active WebdriverIO Browser object. * @return A Promise that resolves to the ID of the currently selected block. @@ -136,6 +155,17 @@ export async function getSelectedBlockId(browser) { }); } +/** + * @param browser The active WebdriverIO Browser object. + * @return A Promise that resolves to the ID of the currently selected block. + */ +export async function getSelectedBlockType(browser) { + return await browser.execute(() => { + // Note: selected is an ICopyable and I am assuming that it is a BlockSvg. + return Blockly.common.getSelected()?.type; + }); +} + /** * @param browser The active WebdriverIO Browser object. * @return A Promise that resolves to the selected block's root SVG element, @@ -165,41 +195,57 @@ export async function getBlockElementById(browser, id) { * causes problems if it has holes (e.g. statement inputs). Instead, this tries * to get the first text field on the block. It falls back on the block's SVG root. * @param browser The active WebdriverIO Browser object. - * @param block The block to click, as an interactable element. + * @param blockId The id of the block to click, as an interactable element. * @param clickOptions The options to pass to webdriverio's element.click function. * @return A Promise that resolves when the actions are completed. */ -export async function clickBlock(browser, block, clickOptions) { - const findableId = 'clickTargetElement'; +export async function clickBlock(browser, blockId, clickOptions) { // In the browser context, find the element that we want and give it a findable ID. - await browser.execute( - (blockId, newElemId) => { - const block = Blockly.getMainWorkspace().getBlockById(blockId); - for (const input of block.inputList) { - for (const field of input.fieldRow) { - if (field instanceof Blockly.FieldLabel) { - field.getSvgRoot().id = newElemId; - return; + const elem = await getTargetableBlockElement(browser, blockId, false); + await elem.click(clickOptions); +} + +/** + * Find an element on the block that is suitable for a click or drag. + * + * We can't always use the block's SVG root because clicking will always happen + * in the middle of the block's bounds (including children) by default, which + * causes problems if it has holes (e.g. statement inputs). Instead, this tries + * to get the first text field on the block. It falls back on the block's SVG root. + * @param browser The active WebdriverIO Browser object. + * @param blockId The id of the block to click, as an interactable element. + * @param toolbox True if this block is in the toolbox (which must be open already). + * @return A Promise that returns an appropriate element. + */ +async function getTargetableBlockElement(browser, blockId, toolbox) { + const id = await browser.execute( + (blockId, toolbox, newElemId) => { + const ws = toolbox + ? Blockly.getMainWorkspace().getFlyout().getWorkspace() + : Blockly.getMainWorkspace(); + const block = ws.getBlockById(blockId); + // Ensure the block we want to click/drag is within the viewport. + ws.scrollBoundsIntoView(block.getBoundingRectangleWithoutChildren(), 10); + if (!block.isCollapsed()) { + for (const input of block.inputList) { + for (const field of input.fieldRow) { + if (field instanceof Blockly.FieldLabel) { + // Expose the id of the element we want to target + field.getSvgRoot().setAttribute('data-id', field.id_); + return field.getSvgRoot().id; + } } } } - // No label field found. Fall back to the block's SVG root. - block.getSvgRoot().id = findableId; + // No label field found. Fall back to the block's SVG root, which should + // already use the block id. + return block.id; }, - block.id, - findableId, + blockId, + toolbox, ); - // In the test context, get the Webdriverio Element that we've identified. - const elem = await browser.$(`#${findableId}`); - - await elem.click(clickOptions); - - // In the browser context, remove the ID. - await browser.execute((elemId) => { - const clickElem = document.getElementById(elemId); - clickElem.removeAttribute('id'); - }, findableId); + return await getBlockElementById(browser, id); } /** @@ -208,7 +254,7 @@ export async function clickBlock(browser, block, clickOptions) { * @return A Promise that resolves when the actions are completed. */ export async function clickWorkspace(browser) { - const workspace = await browser.$('#blocklyDiv > div > svg.blocklySvg > g'); + const workspace = await browser.$('svg.blocklySvg > g'); await workspace.click(); await browser.pause(PAUSE_TIME); } @@ -246,27 +292,14 @@ export async function getCategory(browser, categoryName) { } /** - * @param browser The active WebdriverIO Browser object. - * @param categoryName The name of the toolbox category to search. - * @param n Which block to select, 0-indexed from the top of the category. - * @return A Promise that resolves to the root element of the nth - * block in the given category. - */ -export async function getNthBlockOfCategory(browser, categoryName, n) { - const category = await getCategory(browser, categoryName); - await category.click(); - const block = ( - await browser.$$(`.blocklyFlyout .blocklyBlockCanvas > .blocklyDraggable`) - )[n]; - return block; -} - -/** + * Opens the specified category, finds the first block of the given type, + * scrolls it into view, and returns a draggable element on that block. + * * @param browser The active WebdriverIO Browser object. * @param categoryName The name of the toolbox category to search. * Null if the toolbox has no categories (simple). * @param blockType The type of the block to search for. - * @return A Promise that resolves to the root element of the first + * @return A Promise that resolves to a draggable element of the first * block with the given type in the given category. */ export async function getBlockTypeFromCategory( @@ -279,13 +312,14 @@ export async function getBlockTypeFromCategory( await category.click(); } + await browser.pause(PAUSE_TIME); const id = await browser.execute((blockType) => { - return Blockly.getMainWorkspace() - .getFlyout() - .getWorkspace() - .getBlocksByType(blockType)[0].id; + const ws = Blockly.getMainWorkspace().getFlyout().getWorkspace(); + const block = ws.getBlocksByType(blockType)[0]; + ws.scrollBoundsIntoView(block.getBoundingRectangleWithoutChildren()); + return block.id; }, blockType); - return getBlockElementById(browser, id); + return getTargetableBlockElement(browser, id, true); } /** @@ -440,7 +474,16 @@ export async function switchRTL(browser) { * created block. */ export async function dragNthBlockFromFlyout(browser, categoryName, n, x, y) { - const flyoutBlock = await getNthBlockOfCategory(browser, categoryName, n); + const category = await getCategory(browser, categoryName); + await category.click(); + + await browser.pause(PAUSE_TIME); + const id = await browser.execute((n) => { + const ws = Blockly.getMainWorkspace().getFlyout().getWorkspace(); + const block = ws.getTopBlocks(true)[n]; + return block.id; + }, n); + const flyoutBlock = await getTargetableBlockElement(browser, id, true); await flyoutBlock.dragAndDrop({x: x, y: y}); return await getSelectedBlockElement(browser); } @@ -473,12 +516,13 @@ export async function dragBlockTypeFromFlyout( type, ); await flyoutBlock.dragAndDrop({x: x, y: y}); + await browser.pause(PAUSE_TIME); return await getSelectedBlockElement(browser); } /** - * Drags the specified block type from the mutator flyout of the given block and - * returns the root element of the block. + * Drags the specified block type from the mutator flyout of the given block + * and returns the root element of the block. * * @param browser The active WebdriverIO Browser object. * @param mutatorBlock The block with the mutator attached that we want to drag @@ -512,7 +556,18 @@ export async function dragBlockFromMutatorFlyout( ); const flyoutBlock = await getBlockElementById(browser, id); await flyoutBlock.dragAndDrop({x: x, y: y}); - return await getSelectedBlockElement(browser); + + const draggedBlockId = await browser.execute( + (mutatorBlockId, blockType) => { + return Blockly.getMainWorkspace() + .getBlockById(mutatorBlockId) + .mutator.getWorkspace() + .getBlocksByType(blockType)[0].id; + }, + mutatorBlock.id, + type, + ); + return await getBlockElementById(browser, draggedBlockId); } /** @@ -526,8 +581,9 @@ export async function dragBlockFromMutatorFlyout( * @return A Promise that resolves when the actions are completed. */ export async function contextMenuSelect(browser, block, itemText) { - await clickBlock(browser, block, {button: 2}); + await clickBlock(browser, block.id, {button: 2}); + await browser.pause(PAUSE_TIME); const item = await browser.$(`div=${itemText}`); await item.waitForExist(); await item.click(); @@ -565,26 +621,3 @@ export async function getAllBlocks(browser) { })); }); } - -/** - * Find the flyout's scrollbar and scroll by the specified amount. - * This makes several assumptions: - * - A flyout with a valid scrollbar exists, is open, and is in view. - * - The workspace has a trash can, which means it has a second (hidden) flyout. - * @param browser The active WebdriverIO Browser object. - * @param xDelta How far to drag the flyout in the x direction. Positive is right. - * @param yDelta How far to drag the flyout in the y direction. Positive is down. - * @return A Promise that resolves when the actions are completed. - */ -export async function scrollFlyout(browser, xDelta, yDelta) { - // There are two flyouts on the playground workspace: one for the trash can - // and one for the toolbox. We want the second one. - // This assumes there is only one scrollbar handle in the flyout, but it could - // be either horizontal or vertical. - await browser.pause(PAUSE_TIME); - const scrollbarHandle = await browser - .$$(`.blocklyFlyoutScrollbar`)[1] - .$(`rect.blocklyScrollbarHandle`); - await scrollbarHandle.dragAndDrop({x: xDelta, y: yDelta}); - await browser.pause(PAUSE_TIME); -} diff --git a/tests/browser/test/toolbox_drag_test.mjs b/packages/blockly/tests/browser/test/toolbox_drag_test.mjs similarity index 75% rename from tests/browser/test/toolbox_drag_test.mjs rename to packages/blockly/tests/browser/test/toolbox_drag_test.mjs index 742872d9339..5687febbb83 100644 --- a/tests/browser/test/toolbox_drag_test.mjs +++ b/packages/blockly/tests/browser/test/toolbox_drag_test.mjs @@ -9,11 +9,12 @@ */ import * as chai from 'chai'; +import {Key} from 'webdriverio'; import { + getBlockTypeFromCategory, getCategory, PAUSE_TIME, screenDirection, - scrollFlyout, testFileLocations, testSetup, } from './test_setup.mjs'; @@ -57,28 +58,29 @@ const testCategories = [ ]; /** - * Check whether an element is fully inside the bounds of the Blockly div. You can use this - * to determine whether a block on the workspace or flyout is inside the Blockly div. - * This does not check whether there are other Blockly elements (such as a toolbox or - * flyout) on top of the element. A partially visible block is considered out of bounds. + * Get the type of the nth block in the specified category. * @param browser The active WebdriverIO Browser object. - * @param element The element to look for. - * @returns A Promise resolving to true if the element is in bounds and false otherwise. + * @param categoryName The name of the category to inspect. + * @param n The index of the block to get + * @returns A Promise resolving to the type the block in the specified + * category's flyout at index i. */ -async function elementInBounds(browser, element) { - return await browser.execute((elem) => { - const rect = elem.getBoundingClientRect(); - - const blocklyDiv = document.getElementById('blocklyDiv'); - const blocklyRect = blocklyDiv.getBoundingClientRect(); +async function getNthBlockType(browser, categoryName, n) { + const category = await getCategory(browser, categoryName); + await category.click(); + await browser.pause(PAUSE_TIME); - const vertInView = - rect.top >= blocklyRect.top && rect.bottom <= blocklyRect.bottom; - const horInView = - rect.left >= blocklyRect.left && rect.right <= blocklyRect.right; + const blockType = await browser.execute((i) => { + return Blockly.getMainWorkspace() + .getFlyout() + .getWorkspace() + .getTopBlocks(false)[i].type; + }, n); - return vertInView && horInView; - }, element); + // Unicode escape to close flyout. + await browser.keys([Key.Escape]); + await browser.pause(PAUSE_TIME); + return blockType; } /** @@ -101,7 +103,7 @@ async function getBlockCount(browser, categoryName) { }); // Unicode escape to close flyout. - await browser.keys(['\uE00C']); + await browser.keys([Key.Escape]); await browser.pause(PAUSE_TIME); return blockCount; } @@ -141,18 +143,17 @@ async function openCategories(browser, categoryList, directionMultiplier) { await category.click(); if (await isBlockDisabled(browser, i)) { // Unicode escape to close flyout. - await browser.keys(['\uE00C']); + await browser.keys([Key.Escape]); await browser.pause(PAUSE_TIME); continue; } - const flyoutBlock = await browser.$( - `.blocklyFlyout .blocklyBlockCanvas > g:nth-child(${3 + i * 2})`, + const blockType = await getNthBlockType(browser, categoryName, i); + const blockElem = await getBlockTypeFromCategory( + browser, + categoryName, + blockType, ); - while (!(await elementInBounds(browser, flyoutBlock))) { - await scrollFlyout(browser, 0, 50); - } - - await flyoutBlock.dragAndDrop({x: directionMultiplier * 50, y: 0}); + await blockElem.dragAndDrop({x: 50 * directionMultiplier, y: 20}); await browser.pause(PAUSE_TIME); // Should be one top level block on the workspace. const topBlockCount = await browser.execute(() => { @@ -178,6 +179,8 @@ async function openCategories(browser, categoryList, directionMultiplier) { chai.assert.equal(failureCount, 0); } +// TODO (#9217) These take too long to run and are very flakey. Need to pull +// these out into their own test runner. suite('Open toolbox categories', function () { this.timeout(0); @@ -204,4 +207,13 @@ suite('Open toolbox categories', function () { ); await openCategories(this.browser, testCategories, screenDirection.RTL); }); + + test('clicking the toolbox itself does not open the flyout', async function () { + this.browser = await testSetup(testFileLocations.PLAYGROUND); + await this.browser.$('.blocklyToolbox').click(); + const flyoutOpen = await this.browser.execute(() => { + return Blockly.getMainWorkspace().getFlyout().isVisible(); + }); + chai.assert.isFalse(flyoutOpen); + }); }); diff --git a/tests/browser/test/workspace_comment_test.mjs b/packages/blockly/tests/browser/test/workspace_comment_test.mjs similarity index 95% rename from tests/browser/test/workspace_comment_test.mjs rename to packages/blockly/tests/browser/test/workspace_comment_test.mjs index 516523276f7..db42f30991a 100644 --- a/tests/browser/test/workspace_comment_test.mjs +++ b/packages/blockly/tests/browser/test/workspace_comment_test.mjs @@ -206,13 +206,13 @@ suite('Workspace comments', function () { '.blocklyComment .blocklyResizeHandle', ); await resizeHandle.dragAndDrop(delta); - - chai.assert.deepEqual( - await getCommentSize(this.browser, commentId), - { - width: origSize.width + delta.x, - height: origSize.height + delta.y, - }, + const newSize = await getCommentSize(this.browser, commentId); + chai.assert.isTrue( + Math.abs(newSize.width - (origSize.width + delta.x)) < 1, + 'Expected the comment model size to match the resized size', + ); + chai.assert.isTrue( + Math.abs(newSize.height - (origSize.height + delta.y)) < 1, 'Expected the comment model size to match the resized size', ); }); diff --git a/tests/compile/index.html b/packages/blockly/tests/compile/index.html similarity index 100% rename from tests/compile/index.html rename to packages/blockly/tests/compile/index.html diff --git a/tests/compile/main.js b/packages/blockly/tests/compile/main.js similarity index 100% rename from tests/compile/main.js rename to packages/blockly/tests/compile/main.js diff --git a/tests/compile/test_blocks.js b/packages/blockly/tests/compile/test_blocks.js similarity index 100% rename from tests/compile/test_blocks.js rename to packages/blockly/tests/compile/test_blocks.js diff --git a/tests/compile/webdriver.js b/packages/blockly/tests/compile/webdriver.js similarity index 100% rename from tests/compile/webdriver.js rename to packages/blockly/tests/compile/webdriver.js diff --git a/tests/generators/functions.xml b/packages/blockly/tests/generators/functions.xml similarity index 100% rename from tests/generators/functions.xml rename to packages/blockly/tests/generators/functions.xml diff --git a/tests/generators/golden/generated.dart b/packages/blockly/tests/generators/golden/generated.dart similarity index 100% rename from tests/generators/golden/generated.dart rename to packages/blockly/tests/generators/golden/generated.dart diff --git a/tests/generators/golden/generated.js b/packages/blockly/tests/generators/golden/generated.js similarity index 100% rename from tests/generators/golden/generated.js rename to packages/blockly/tests/generators/golden/generated.js diff --git a/tests/generators/golden/generated.lua b/packages/blockly/tests/generators/golden/generated.lua similarity index 100% rename from tests/generators/golden/generated.lua rename to packages/blockly/tests/generators/golden/generated.lua diff --git a/tests/generators/golden/generated.php b/packages/blockly/tests/generators/golden/generated.php similarity index 100% rename from tests/generators/golden/generated.php rename to packages/blockly/tests/generators/golden/generated.php diff --git a/tests/generators/golden/generated.py b/packages/blockly/tests/generators/golden/generated.py similarity index 100% rename from tests/generators/golden/generated.py rename to packages/blockly/tests/generators/golden/generated.py diff --git a/tests/generators/index.html b/packages/blockly/tests/generators/index.html similarity index 100% rename from tests/generators/index.html rename to packages/blockly/tests/generators/index.html diff --git a/tests/generators/lists.xml b/packages/blockly/tests/generators/lists.xml similarity index 100% rename from tests/generators/lists.xml rename to packages/blockly/tests/generators/lists.xml diff --git a/tests/generators/logic.xml b/packages/blockly/tests/generators/logic.xml similarity index 100% rename from tests/generators/logic.xml rename to packages/blockly/tests/generators/logic.xml diff --git a/tests/generators/loops1.xml b/packages/blockly/tests/generators/loops1.xml similarity index 100% rename from tests/generators/loops1.xml rename to packages/blockly/tests/generators/loops1.xml diff --git a/tests/generators/loops2.xml b/packages/blockly/tests/generators/loops2.xml similarity index 100% rename from tests/generators/loops2.xml rename to packages/blockly/tests/generators/loops2.xml diff --git a/tests/generators/loops3.xml b/packages/blockly/tests/generators/loops3.xml similarity index 100% rename from tests/generators/loops3.xml rename to packages/blockly/tests/generators/loops3.xml diff --git a/tests/generators/math.xml b/packages/blockly/tests/generators/math.xml similarity index 100% rename from tests/generators/math.xml rename to packages/blockly/tests/generators/math.xml diff --git a/tests/generators/text.xml b/packages/blockly/tests/generators/text.xml similarity index 100% rename from tests/generators/text.xml rename to packages/blockly/tests/generators/text.xml diff --git a/tests/generators/unittest.js b/packages/blockly/tests/generators/unittest.js similarity index 100% rename from tests/generators/unittest.js rename to packages/blockly/tests/generators/unittest.js diff --git a/tests/generators/unittest_dart.js b/packages/blockly/tests/generators/unittest_dart.js similarity index 100% rename from tests/generators/unittest_dart.js rename to packages/blockly/tests/generators/unittest_dart.js diff --git a/tests/generators/unittest_javascript.js b/packages/blockly/tests/generators/unittest_javascript.js similarity index 100% rename from tests/generators/unittest_javascript.js rename to packages/blockly/tests/generators/unittest_javascript.js diff --git a/tests/generators/unittest_lua.js b/packages/blockly/tests/generators/unittest_lua.js similarity index 100% rename from tests/generators/unittest_lua.js rename to packages/blockly/tests/generators/unittest_lua.js diff --git a/tests/generators/unittest_php.js b/packages/blockly/tests/generators/unittest_php.js similarity index 100% rename from tests/generators/unittest_php.js rename to packages/blockly/tests/generators/unittest_php.js diff --git a/tests/generators/unittest_python.js b/packages/blockly/tests/generators/unittest_python.js similarity index 100% rename from tests/generators/unittest_python.js rename to packages/blockly/tests/generators/unittest_python.js diff --git a/tests/generators/variables.xml b/packages/blockly/tests/generators/variables.xml similarity index 100% rename from tests/generators/variables.xml rename to packages/blockly/tests/generators/variables.xml diff --git a/tests/generators/webdriver.js b/packages/blockly/tests/generators/webdriver.js similarity index 100% rename from tests/generators/webdriver.js rename to packages/blockly/tests/generators/webdriver.js diff --git a/tests/media/200px.png b/packages/blockly/tests/media/200px.png similarity index 100% rename from tests/media/200px.png rename to packages/blockly/tests/media/200px.png diff --git a/tests/media/30px.png b/packages/blockly/tests/media/30px.png similarity index 100% rename from tests/media/30px.png rename to packages/blockly/tests/media/30px.png diff --git a/tests/media/50px.png b/packages/blockly/tests/media/50px.png similarity index 100% rename from tests/media/50px.png rename to packages/blockly/tests/media/50px.png diff --git a/tests/media/a.png b/packages/blockly/tests/media/a.png similarity index 100% rename from tests/media/a.png rename to packages/blockly/tests/media/a.png diff --git a/tests/media/arrow.png b/packages/blockly/tests/media/arrow.png similarity index 100% rename from tests/media/arrow.png rename to packages/blockly/tests/media/arrow.png diff --git a/tests/media/b.png b/packages/blockly/tests/media/b.png similarity index 100% rename from tests/media/b.png rename to packages/blockly/tests/media/b.png diff --git a/tests/media/c.png b/packages/blockly/tests/media/c.png similarity index 100% rename from tests/media/c.png rename to packages/blockly/tests/media/c.png diff --git a/tests/media/d.png b/packages/blockly/tests/media/d.png similarity index 100% rename from tests/media/d.png rename to packages/blockly/tests/media/d.png diff --git a/tests/media/e.png b/packages/blockly/tests/media/e.png similarity index 100% rename from tests/media/e.png rename to packages/blockly/tests/media/e.png diff --git a/tests/media/f.png b/packages/blockly/tests/media/f.png similarity index 100% rename from tests/media/f.png rename to packages/blockly/tests/media/f.png diff --git a/tests/media/g.png b/packages/blockly/tests/media/g.png similarity index 100% rename from tests/media/g.png rename to packages/blockly/tests/media/g.png diff --git a/tests/media/h.png b/packages/blockly/tests/media/h.png similarity index 100% rename from tests/media/h.png rename to packages/blockly/tests/media/h.png diff --git a/tests/media/i.png b/packages/blockly/tests/media/i.png similarity index 100% rename from tests/media/i.png rename to packages/blockly/tests/media/i.png diff --git a/tests/media/j.png b/packages/blockly/tests/media/j.png similarity index 100% rename from tests/media/j.png rename to packages/blockly/tests/media/j.png diff --git a/tests/media/k.png b/packages/blockly/tests/media/k.png similarity index 100% rename from tests/media/k.png rename to packages/blockly/tests/media/k.png diff --git a/tests/media/l.png b/packages/blockly/tests/media/l.png similarity index 100% rename from tests/media/l.png rename to packages/blockly/tests/media/l.png diff --git a/tests/media/m.png b/packages/blockly/tests/media/m.png similarity index 100% rename from tests/media/m.png rename to packages/blockly/tests/media/m.png diff --git a/tests/migration/renamings.schema.json b/packages/blockly/tests/migration/renamings.schema.json similarity index 100% rename from tests/migration/renamings.schema.json rename to packages/blockly/tests/migration/renamings.schema.json diff --git a/tests/migration/validate-renamings.mjs b/packages/blockly/tests/migration/validate-renamings.mjs similarity index 100% rename from tests/migration/validate-renamings.mjs rename to packages/blockly/tests/migration/validate-renamings.mjs diff --git a/tests/mocha/.mocharc.js b/packages/blockly/tests/mocha/.mocharc.js similarity index 100% rename from tests/mocha/.mocharc.js rename to packages/blockly/tests/mocha/.mocharc.js diff --git a/tests/mocha/block_json_test.js b/packages/blockly/tests/mocha/block_json_test.js similarity index 99% rename from tests/mocha/block_json_test.js rename to packages/blockly/tests/mocha/block_json_test.js index 4baccef6b7b..31abd6e3484 100644 --- a/tests/mocha/block_json_test.js +++ b/packages/blockly/tests/mocha/block_json_test.js @@ -5,7 +5,7 @@ */ import {Align} from '../../build/src/core/inputs/align.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/block_test.js b/packages/blockly/tests/mocha/block_test.js similarity index 92% rename from tests/mocha/block_test.js rename to packages/blockly/tests/mocha/block_test.js index a489fb3e3c5..ed41a728c30 100644 --- a/tests/mocha/block_test.js +++ b/packages/blockly/tests/mocha/block_test.js @@ -4,12 +4,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as common from '../../build/src/core/common.js'; import {ConnectionType} from '../../build/src/core/connection_type.js'; import {EventType} from '../../build/src/core/events/type.js'; import * as eventUtils from '../../build/src/core/events/utils.js'; +import {IconType} from '../../build/src/core/icons/icon_types.js'; import {EndRowInput} from '../../build/src/core/inputs/end_row_input.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {isCommentIcon} from '../../build/src/core/interfaces/i_comment_icon.js'; +import {Size} from '../../build/src/core/utils/size.js'; +import {assert} from '../../node_modules/chai/index.js'; import {createRenderedBlock} from './test_helpers/block_definitions.js'; import { createChangeListenerSpy, @@ -199,6 +201,35 @@ suite('Blocks', function () { assertUnpluggedHealFailed(blocks); }); + test('Disconnect top of stack with immovable sibling', function () { + this.blocks.B.setMovable(false); + this.blocks.A.unplug(true); + assert.equal(this.blocks.A.nextConnection.targetBlock(), this.blocks.B); + assert.isNull(this.blocks.B.nextConnection.targetBlock()); + assert.isNull(this.blocks.C.previousConnection.targetBlock()); + }); + test('Heal with immovable sibling mid-stack', function () { + const blockD = this.workspace.newBlock('stack_block', 'd'); + this.blocks.C.nextConnection.connect(blockD.previousConnection); + this.blocks.C.setMovable(false); + this.blocks.B.unplug(true); + assert.equal(this.blocks.A.nextConnection.targetBlock(), blockD); + assert.equal(this.blocks.B.nextConnection.targetBlock(), this.blocks.C); + assert.isNull(this.blocks.C.nextConnection.targetBlock()); + }); + test('Heal with immovable sibling and shadow sibling mid-stack', function () { + const blockD = this.workspace.newBlock('stack_block', 'd'); + const blockE = this.workspace.newBlock('stack_block', 'e'); + this.blocks.C.nextConnection.connect(blockD.previousConnection); + blockD.nextConnection.connect(blockE.previousConnection); + this.blocks.C.setMovable(false); + blockD.setShadow(true); + this.blocks.B.unplug(true); + assert.equal(this.blocks.A.nextConnection.targetBlock(), blockE); + assert.equal(this.blocks.B.nextConnection.targetBlock(), this.blocks.C); + assert.equal(this.blocks.C.nextConnection.targetBlock(), blockD); + assert.isNull(blockD.nextConnection.targetBlock()); + }); test('Child is shadow', function () { const blocks = this.blocks; blocks.C.setShadow(true); @@ -463,20 +494,6 @@ suite('Blocks', function () { teardown(function () { workspaceTeardown.call(this, this.workspace); }); - - test('Disposing selected shadow unhighlights parent', function () { - const parentBlock = this.parentBlock; - common.setSelected(this.shadowChild); - assert.isTrue( - parentBlock.pathObject.svgRoot.classList.contains('blocklySelected'), - 'Expected parent to be highlighted after selecting shadow child', - ); - this.shadowChild.dispose(); - assert.isFalse( - parentBlock.pathObject.svgRoot.classList.contains('blocklySelected'), - 'Expected parent to be unhighlighted after deleting shadow child', - ); - }); }); }); @@ -1105,6 +1122,18 @@ suite('Blocks', function () { ); this.textJoinBlock = this.printBlock.getInputTargetBlock('TEXT'); this.textBlock = this.textJoinBlock.getInputTargetBlock('ADD0'); + this.extraTopBlock = Blockly.Xml.domToBlock( + Blockly.utils.xml.textToDom(` + + + + drag me + + + `), + this.workspace, + ); + this.extraNestedBlock = this.extraTopBlock.getInputTargetBlock('TEXT'); }); function assertBlockIsOnlyChild(parent, child, inputName) { @@ -1116,6 +1145,10 @@ suite('Blocks', function () { assert.equal(nonParent.getChildren().length, 0); assert.isNull(nonParent.getInputTargetBlock('TEXT')); assert.isNull(orphan.getParent()); + assert.equal( + orphan.getSvgRoot().parentElement, + orphan.workspace.getCanvas(), + ); } function assertOriginalSetup() { assertBlockIsOnlyChild(this.printBlock, this.textJoinBlock, 'TEXT'); @@ -1187,6 +1220,27 @@ suite('Blocks', function () { ); assertNonParentAndOrphan(this.textJoinBlock, this.textBlock, 'ADD0'); }); + test('Setting parent to null with dragging block', function () { + this.extraTopBlock.setDragging(true); + this.textBlock.outputConnection.disconnect(); + assert.doesNotThrow( + this.textBlock.setParent.bind(this.textBlock, null), + ); + assertNonParentAndOrphan(this.textJoinBlock, this.textBlock, 'ADD0'); + assert.equal( + this.textBlock.getSvgRoot().nextSibling, + this.extraTopBlock.getSvgRoot(), + ); + }); + test('Setting parent to null with non-top dragging block', function () { + this.extraNestedBlock.setDragging(true); + this.textBlock.outputConnection.disconnect(); + assert.doesNotThrow( + this.textBlock.setParent.bind(this.textBlock, null), + ); + assertNonParentAndOrphan(this.textJoinBlock, this.textBlock, 'ADD0'); + assert.equal(this.textBlock.getSvgRoot().nextSibling, null); + }); test('Setting parent to null without disconnecting', function () { assert.throws(this.textBlock.setParent.bind(this.textBlock, null)); assertOriginalSetup.call(this); @@ -1404,9 +1458,9 @@ suite('Blocks', function () { }); suite('Constructing registered comment classes', function () { - class MockComment extends MockIcon { + class MockComment extends MockBubbleIcon { getType() { - return Blockly.icons.IconType.COMMENT; + return IconType.COMMENT; } setText() {} @@ -1418,14 +1472,12 @@ suite('Blocks', function () { setBubbleSize() {} getBubbleSize() { - return Blockly.utils.Size(0, 0); + return Size(0, 0); } - bubbleIsVisible() { - return true; - } + setBubbleLocation() {} - setBubbleVisible() {} + getBubbleLocation() {} saveState() { return {}; @@ -1434,6 +1486,10 @@ suite('Blocks', function () { loadState() {} } + if (!isCommentIcon(new MockComment())) { + throw new TypeError('MockComment not an ICommentIcon'); + } + setup(function () { this.workspace = Blockly.inject('blocklyDiv', {}); @@ -1840,6 +1896,79 @@ suite('Blocks', function () { }); }); + suite('Warning icons and collapsing', function () { + setup(function () { + this.workspace = Blockly.inject('blocklyDiv'); + this.parentBlock = Blockly.serialization.blocks.append( + { + 'type': 'statement_block', + 'inputs': { + 'STATEMENT': { + 'block': { + 'type': 'statement_block', + }, + }, + }, + }, + this.workspace, + ); + this.parentBlock.initSvg(); + this.parentBlock.render(); + + this.childBlock = this.parentBlock.getInputTargetBlock('STATEMENT'); + this.childBlock.initSvg(); + this.childBlock.render(); + }); + + teardown(function () { + workspaceTeardown.call(this, this.workspace); + }); + + test('Adding a warning to a child block does not affect the parent', function () { + const text = 'Warning Text'; + this.childBlock.setWarningText(text); + const icon = this.parentBlock.getIcon(Blockly.icons.WarningIcon.TYPE); + assert.isUndefined( + icon, + "Setting a child block's warning should not add a warning to the parent", + ); + }); + + test('Warnings are added and removed when collapsing a stack with warnings', function () { + const text = 'Warning Text'; + + this.childBlock.setWarningText(text); + + this.parentBlock.setCollapsed(true); + let icon = this.parentBlock.getIcon(Blockly.icons.WarningIcon.TYPE); + assert.exists(icon?.getText(), 'Expected warning icon text to be set'); + + this.parentBlock.setCollapsed(false); + icon = this.parentBlock.getIcon(Blockly.icons.WarningIcon.TYPE); + assert.isUndefined( + icon, + 'Warning should be removed from parent after expanding', + ); + }); + + test('Collapsing a block should not inherit warnings from following siblings', function () { + const nextBlock = createRenderedBlock( + this.workspace, + 'statement_block', + ); + this.childBlock.nextConnection.connect(nextBlock.previousConnection); + nextBlock.setWarningText('Warning Text'); + + this.childBlock.setCollapsed(true); + + const icon = this.childBlock.getIcon(Blockly.icons.WarningIcon.TYPE); + assert.isUndefined( + icon, + 'Collapsed block should not show warnings from following siblings', + ); + }); + }); + suite('Bubbles and collapsing', function () { setup(function () { this.workspace = Blockly.inject('blocklyDiv'); @@ -1976,6 +2105,7 @@ suite('Blocks', function () { ], }, ]); + this.variableMap = this.workspace.getVariableMap(); }); teardown(function () { eventUtils.enable(); @@ -2314,13 +2444,14 @@ suite('Blocks', function () { assertCollapsed(blockA); }); }); + suite('Renaming Vars', function () { test('Simple Rename', function () { const blockA = createRenderedBlock(this.workspace, 'variable_block'); blockA.setCollapsed(true); - const variable = this.workspace.getVariable('x', ''); - this.workspace.renameVariableById(variable.getId(), 'y'); + const variable = this.workspace.getVariableMap().getVariable('x', ''); + this.variableMap.renameVariable(variable, 'y'); this.clock.runAll(); assertCollapsed(blockA, 'y'); @@ -2329,8 +2460,8 @@ suite('Blocks', function () { const blockA = createRenderedBlock(this.workspace, 'variable_block'); blockA.setCollapsed(true); - const variable = this.workspace.createVariable('y'); - this.workspace.renameVariableById(variable.getId(), 'X'); + const variable = this.variableMap.createVariable('y'); + this.variableMap.renameVariable(variable, 'X'); this.clock.runAll(); assertCollapsed(blockA, 'X'); @@ -2769,4 +2900,50 @@ suite('Blocks', function () { assert.equal(block.inputList[1].fieldRow[0].getValue(), 'Row2'); }); }); + + suite('Dragging', function () { + setup(function () { + this.workspace = Blockly.inject('blocklyDiv'); + this.blocks = createTestBlocks(this.workspace, false); + for (const block of Object.values(this.blocks)) { + block.initSvg(); + block.render(); + } + }); + test('Bubbles are moved to drag layer along with their blocks', async function () { + this.blocks.A.setCommentText('a'); + this.blocks.B.setCommentText('b'); + this.blocks.C.setCommentText('c'); + const v1 = this.blocks.A.getIcon( + Blockly.icons.IconType.COMMENT, + ).setBubbleVisible(true); + const v2 = this.blocks.B.getIcon( + Blockly.icons.IconType.COMMENT, + ).setBubbleVisible(true); + const v3 = this.blocks.C.getIcon( + Blockly.icons.IconType.COMMENT, + ).setBubbleVisible(true); + + this.clock.tick(1000); + await Promise.all([v1, v2, v3]); + + this.blocks.B.startDrag(); + + // Block A stays put and should have its comment stay on the bubble layer. + assert.equal( + this.blocks.A.getIcon(Blockly.icons.IconType.COMMENT) + .getBubble() + .getSvgRoot().parentElement, + this.blocks.A.workspace.getLayerManager()?.getBubbleLayer(), + ); + + // Block B moves to the drag layer and its comment should follow. + assert.equal( + this.blocks.B.getIcon(Blockly.icons.IconType.COMMENT) + .getBubble() + .getSvgRoot().parentElement, + this.blocks.B.workspace.getLayerManager()?.getDragLayer(), + ); + }); + }); }); diff --git a/tests/mocha/blocks/lists_test.js b/packages/blockly/tests/mocha/blocks/lists_test.js similarity index 99% rename from tests/mocha/blocks/lists_test.js rename to packages/blockly/tests/mocha/blocks/lists_test.js index 490109d22ca..e749fae90a7 100644 --- a/tests/mocha/blocks/lists_test.js +++ b/packages/blockly/tests/mocha/blocks/lists_test.js @@ -5,7 +5,7 @@ */ import {ConnectionType} from '../../../build/src/core/connection_type.js'; -import {assert} from '../../../node_modules/chai/chai.js'; +import {assert} from '../../../node_modules/chai/index.js'; import {defineStatementBlock} from '../test_helpers/block_definitions.js'; import {runSerializationTestSuite} from '../test_helpers/serialization.js'; import { diff --git a/tests/mocha/blocks/logic_ternary_test.js b/packages/blockly/tests/mocha/blocks/logic_ternary_test.js similarity index 99% rename from tests/mocha/blocks/logic_ternary_test.js rename to packages/blockly/tests/mocha/blocks/logic_ternary_test.js index 71920935981..3d343a7caec 100644 --- a/tests/mocha/blocks/logic_ternary_test.js +++ b/packages/blockly/tests/mocha/blocks/logic_ternary_test.js @@ -5,7 +5,7 @@ */ import * as eventUtils from '../../../build/src/core/events/utils.js'; -import {assert} from '../../../node_modules/chai/chai.js'; +import {assert} from '../../../node_modules/chai/index.js'; import {runSerializationTestSuite} from '../test_helpers/serialization.js'; import { sharedTestSetup, diff --git a/tests/mocha/blocks/loops_test.js b/packages/blockly/tests/mocha/blocks/loops_test.js similarity index 96% rename from tests/mocha/blocks/loops_test.js rename to packages/blockly/tests/mocha/blocks/loops_test.js index f8d74916c29..eb040c884a7 100644 --- a/tests/mocha/blocks/loops_test.js +++ b/packages/blockly/tests/mocha/blocks/loops_test.js @@ -5,7 +5,7 @@ */ import * as Blockly from '../../../build/src/core/blockly.js'; -import {assert} from '../../../node_modules/chai/chai.js'; +import {assert} from '../../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/blocks/procedures_test.js b/packages/blockly/tests/mocha/blocks/procedures_test.js similarity index 98% rename from tests/mocha/blocks/procedures_test.js rename to packages/blockly/tests/mocha/blocks/procedures_test.js index 4b20662cf93..779684be125 100644 --- a/tests/mocha/blocks/procedures_test.js +++ b/packages/blockly/tests/mocha/blocks/procedures_test.js @@ -5,7 +5,7 @@ */ import * as Blockly from '../../../build/src/core/blockly.js'; -import {assert} from '../../../node_modules/chai/chai.js'; +import {assert} from '../../../node_modules/chai/index.js'; import {defineRowBlock} from '../test_helpers/block_definitions.js'; import { assertCallBlockStructure, @@ -26,13 +26,14 @@ suite('Procedures', function () { setup(function () { sharedTestSetup.call(this, {fireEventsNow: false}); this.workspace = Blockly.inject('blocklyDiv', {}); - this.workspace.createVariable('preCreatedVar', '', 'preCreatedVarId'); - this.workspace.createVariable( - 'preCreatedTypedVar', - 'type', - 'preCreatedTypedVarId', - ); + this.workspace + .getVariableMap() + .createVariable('preCreatedVar', '', 'preCreatedVarId'); + this.workspace + .getVariableMap() + .createVariable('preCreatedTypedVar', 'type', 'preCreatedTypedVarId'); defineRowBlock(); + this.variableMap = this.workspace.getVariableMap(); }); teardown(function () { @@ -432,7 +433,7 @@ suite('Procedures', function () { this.clock.runAll(); assert.isNotNull( - this.workspace.getVariable('param1', ''), + this.workspace.getVariableMap().getVariable('param1', ''), 'Expected the old variable to continue to exist', ); }); @@ -452,8 +453,10 @@ suite('Procedures', function () { this.clock.runAll(); mutatorIcon.setBubbleVisible(false); - const variable = this.workspace.getVariable('param1', ''); - this.workspace.renameVariableById(variable.getId(), 'new name'); + const variable = this.workspace + .getVariableMap() + .getVariable('param1', ''); + this.variableMap.renameVariable(variable, 'new name'); assert.isNotNull( defBlock.getField('PARAMS'), @@ -479,8 +482,10 @@ suite('Procedures', function () { .connection.connect(paramBlock.previousConnection); this.clock.runAll(); - const variable = this.workspace.getVariable('param1', ''); - this.workspace.renameVariableById(variable.getId(), 'new name'); + const variable = this.workspace + .getVariableMap() + .getVariable('param1', ''); + this.variableMap.renameVariable(variable, 'new name'); assert.equal( paramBlock.getFieldValue('NAME'), @@ -505,8 +510,10 @@ suite('Procedures', function () { this.clock.runAll(); mutatorIcon.setBubbleVisible(false); - const variable = this.workspace.getVariable('param1', ''); - this.workspace.renameVariableById(variable.getId(), 'new name'); + const variable = this.workspace + .getVariableMap() + .getVariable('param1', ''); + this.variableMap.renameVariable(variable, 'new name'); assert.isNotNull( callBlock.getInput('ARG0'), @@ -534,8 +541,10 @@ suite('Procedures', function () { this.clock.runAll(); mutatorIcon.setBubbleVisible(false); - const variable = this.workspace.getVariable('param1', ''); - this.workspace.renameVariableById(variable.getId(), 'preCreatedVar'); + const variable = this.workspace + .getVariableMap() + .getVariable('param1', ''); + this.variableMap.renameVariable(variable, 'preCreatedVar'); assert.isNotNull( defBlock.getField('PARAMS'), @@ -561,8 +570,10 @@ suite('Procedures', function () { .connection.connect(paramBlock.previousConnection); this.clock.runAll(); - const variable = this.workspace.getVariable('param1', ''); - this.workspace.renameVariableById(variable.getId(), 'preCreatedVar'); + const variable = this.workspace + .getVariableMap() + .getVariable('param1', ''); + this.variableMap.renameVariable(variable, 'preCreatedVar'); assert.equal( paramBlock.getFieldValue('NAME'), @@ -587,8 +598,10 @@ suite('Procedures', function () { this.clock.runAll(); mutatorIcon.setBubbleVisible(false); - const variable = this.workspace.getVariable('param1', ''); - this.workspace.renameVariableById(variable.getId(), 'preCreatedVar'); + const variable = this.workspace + .getVariableMap() + .getVariable('param1', ''); + this.variableMap.renameVariable(variable, 'preCreatedVar'); assert.isNotNull( callBlock.getInput('ARG0'), diff --git a/tests/mocha/blocks/variables_test.js b/packages/blockly/tests/mocha/blocks/variables_test.js similarity index 79% rename from tests/mocha/blocks/variables_test.js rename to packages/blockly/tests/mocha/blocks/variables_test.js index d12691dd476..724e2e543d0 100644 --- a/tests/mocha/blocks/variables_test.js +++ b/packages/blockly/tests/mocha/blocks/variables_test.js @@ -5,7 +5,7 @@ */ import {nameUsedWithConflictingParam} from '../../../build/src/core/variables.js'; -import {assert} from '../../../node_modules/chai/chai.js'; +import {assert} from '../../../node_modules/chai/index.js'; import { MockParameterModelWithVar, MockProcedureModel, @@ -30,11 +30,31 @@ suite('Variables', function () { 'variableTypes': ['', 'type1', 'type2'], }, ], + 'output': null, + }, + // Block for variable setter. + { + 'type': 'set_var_block', + 'message0': '%{BKY_VARIABLES_SET}', + 'args0': [ + { + 'type': 'field_variable', + 'name': 'VAR', + 'variableTypes': ['', 'type1', 'type2'], + }, + { + 'type': 'input_value', + 'name': 'VALUE', + }, + ], + 'previousStatement': null, + 'nextStatement': null, }, ]); - this.workspace.createVariable('foo', 'type1', '1'); - this.workspace.createVariable('bar', 'type1', '2'); - this.workspace.createVariable('baz', 'type1', '3'); + this.variableMap = this.workspace.getVariableMap(); + this.variableMap.createVariable('foo', 'type1', '1'); + this.variableMap.createVariable('bar', 'type1', '2'); + this.variableMap.createVariable('baz', 'type1', '3'); }); teardown(function () { @@ -58,6 +78,21 @@ suite('Variables', function () { return block; } + test('can be deleted when two connected blocks reference the same variable', function () { + const getter = new Blockly.Block(this.workspace, 'get_var_block'); + getter.getField('VAR').setValue('1'); + + const setter = new Blockly.Block(this.workspace, 'set_var_block'); + setter.getField('VAR').setValue('1'); + setter.getInput('VALUE').connection.connect(getter.outputConnection); + + this.variableMap.deleteVariable(this.variableMap.getVariableById('1')); + // Both blocks should have been deleted. + assert.equal(0, this.workspace.getAllBlocks(false).length); + // The variable itself should have been deleted. + assert.equal(this.variableMap.getVariableById('1'), undefined); + }); + suite('allUsedVarModels', function () { test('All used', function () { createTestVarBlock(this.workspace, '1'); @@ -116,12 +151,11 @@ suite('Variables', function () { ); }); }); - suite('getVariable', function () { test('By ID', function () { - const var1 = this.workspace.createVariable('name1', 'type1', 'id1'); - const var2 = this.workspace.createVariable('name2', 'type1', 'id2'); - const var3 = this.workspace.createVariable('name3', 'type2', 'id3'); + const var1 = this.variableMap.createVariable('name1', 'type1', 'id1'); + const var2 = this.variableMap.createVariable('name2', 'type1', 'id2'); + const var3 = this.variableMap.createVariable('name3', 'type2', 'id3'); const result1 = Blockly.Variables.getVariable(this.workspace, 'id1'); const result2 = Blockly.Variables.getVariable(this.workspace, 'id2'); const result3 = Blockly.Variables.getVariable(this.workspace, 'id3'); @@ -132,9 +166,9 @@ suite('Variables', function () { }); test('By name and type', function () { - const var1 = this.workspace.createVariable('name1', 'type1', 'id1'); - const var2 = this.workspace.createVariable('name2', 'type1', 'id2'); - const var3 = this.workspace.createVariable('name3', 'type2', 'id3'); + const var1 = this.variableMap.createVariable('name1', 'type1', 'id1'); + const var2 = this.variableMap.createVariable('name2', 'type1', 'id2'); + const var3 = this.variableMap.createVariable('name3', 'type2', 'id3'); const result1 = Blockly.Variables.getVariable( this.workspace, null, @@ -161,9 +195,9 @@ suite('Variables', function () { }); test('Bad ID with name and type fallback', function () { - const var1 = this.workspace.createVariable('name1', 'type1', 'id1'); - const var2 = this.workspace.createVariable('name2', 'type1', 'id2'); - const var3 = this.workspace.createVariable('name3', 'type2', 'id3'); + const var1 = this.variableMap.createVariable('name1', 'type1', 'id1'); + const var2 = this.variableMap.createVariable('name2', 'type1', 'id2'); + const var3 = this.variableMap.createVariable('name3', 'type2', 'id3'); const result1 = Blockly.Variables.getVariable( this.workspace, 'badId', diff --git a/tests/mocha/clipboard_test.js b/packages/blockly/tests/mocha/clipboard_test.js similarity index 54% rename from tests/mocha/clipboard_test.js rename to packages/blockly/tests/mocha/clipboard_test.js index 0f2d067708a..ff49c0e303c 100644 --- a/tests/mocha/clipboard_test.js +++ b/packages/blockly/tests/mocha/clipboard_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { assertEventFired, createChangeListenerSpy, @@ -61,6 +61,31 @@ suite('Clipboard', function () { ); }); + test('pasting blocks includes next blocks if requested', function () { + const block = Blockly.serialization.blocks.append( + { + 'type': 'controls_if', + 'id': 'blockId', + 'next': { + 'block': { + 'type': 'controls_if', + 'id': 'blockId2', + }, + }, + }, + this.workspace, + ); + assert.equal(this.workspace.getBlocksByType('controls_if').length, 2); + // Both blocks should be copied + const data = block.toCopyData(true); + this.clock.runAll(); + + Blockly.clipboard.paste(data, this.workspace); + this.clock.runAll(); + // After pasting, we should have gone from 2 to 4 blocks. + assert.equal(this.workspace.getBlocksByType('controls_if').length, 4); + }); + test('copied from a mutator pastes them into the mutator', async function () { const block = Blockly.serialization.blocks.append( { @@ -76,7 +101,7 @@ suite('Clipboard', function () { await mutatorIcon.setBubbleVisible(true); const mutatorWorkspace = mutatorIcon.getWorkspace(); const elseIf = mutatorWorkspace.getBlocksByType('controls_if_elseif')[0]; - assert.notEqual(elseIf, undefined); + assert.isDefined(elseIf); assert.lengthOf(mutatorWorkspace.getAllBlocks(), 2); assert.lengthOf(this.workspace.getAllBlocks(), 1); const data = elseIf.toCopyData(); @@ -85,6 +110,34 @@ suite('Clipboard', function () { assert.lengthOf(this.workspace.getAllBlocks(), 1); }); + test('pasting into a mutator flyout pastes into the mutator workspace', async function () { + const block = Blockly.serialization.blocks.append( + { + 'type': 'controls_if', + 'id': 'blockId', + 'extraState': { + 'elseIfCount': 1, + }, + }, + this.workspace, + ); + const mutatorIcon = block.getIcon(Blockly.icons.IconType.MUTATOR); + await mutatorIcon.setBubbleVisible(true); + const mutatorWorkspace = mutatorIcon.getWorkspace(); + const mutatorFlyoutWorkspace = mutatorWorkspace + .getFlyout() + .getWorkspace(); + const elseIf = + mutatorFlyoutWorkspace.getBlocksByType('controls_if_elseif')[0]; + assert.isDefined(elseIf); + assert.lengthOf(mutatorWorkspace.getAllBlocks(), 2); + assert.lengthOf(this.workspace.getAllBlocks(), 1); + const data = elseIf.toCopyData(); + Blockly.clipboard.paste(data, mutatorFlyoutWorkspace); + assert.lengthOf(mutatorWorkspace.getAllBlocks(), 3); + assert.lengthOf(this.workspace.getAllBlocks(), 1); + }); + suite('pasted blocks are placed in unambiguous locations', function () { test('pasted blocks are bumped to not overlap', function () { const block = Blockly.serialization.blocks.append( @@ -104,6 +157,34 @@ suite('Clipboard', function () { ); }); + test('pasted blocks are bumped to not overlap in RTL', function () { + this.workspace.dispose(); + this.workspace = Blockly.inject('blocklyDiv', {rtl: true}); + const block = Blockly.serialization.blocks.append( + { + 'type': 'controls_if', + 'x': 38, + 'y': 13, + }, + this.workspace, + ); + const data = block.toCopyData(); + + const newBlock = Blockly.clipboard.paste(data, this.workspace); + const oldBlockXY = block.getRelativeToSurfaceXY(); + assert.deepEqual( + newBlock.getRelativeToSurfaceXY(), + new Blockly.utils.Coordinate( + oldBlockXY.x - Blockly.config.snapRadius, + oldBlockXY.y + Blockly.config.snapRadius * 2, + ), + ); + + // Restore an LTR workspace. + this.workspace.dispose(); + this.workspace = Blockly.inject('blocklyDiv'); + }); + test('pasted blocks are bumped to be outside the connection snap radius', function () { Blockly.serialization.workspaces.load( { @@ -139,8 +220,26 @@ suite('Clipboard', function () { }); suite('pasting comments', function () { - // TODO: Reenable test when we readd copy-paste. - test.skip('pasted comments are bumped to not overlap', function () { + test('pasted comments are bumped to not overlap', function () { + Blockly.Xml.domToWorkspace( + Blockly.utils.xml.textToDom( + '', + ), + this.workspace, + ); + const comment = this.workspace.getTopComments(false)[0]; + const data = comment.toCopyData(); + + const newComment = Blockly.clipboard.paste(data, this.workspace); + assert.deepEqual( + newComment.getRelativeToSurfaceXY(), + new Blockly.utils.Coordinate(40, 40), + ); + }); + + test('pasted comments are bumped to not overlap in RTL', function () { + this.workspace.dispose(); + this.workspace = Blockly.inject('blocklyDiv', {rtl: true}); Blockly.Xml.domToWorkspace( Blockly.utils.xml.textToDom( '', @@ -151,10 +250,14 @@ suite('Clipboard', function () { const data = comment.toCopyData(); const newComment = Blockly.clipboard.paste(data, this.workspace); + const oldCommentXY = comment.getRelativeToSurfaceXY(); assert.deepEqual( newComment.getRelativeToSurfaceXY(), - new Blockly.utils.Coordinate(60, 60), + new Blockly.utils.Coordinate(oldCommentXY.x - 30, oldCommentXY.y + 30), ); + // Restore an LTR workspace. + this.workspace.dispose(); + this.workspace = Blockly.inject('blocklyDiv'); }); }); }); diff --git a/tests/mocha/comment_deserialization_test.js b/packages/blockly/tests/mocha/comment_deserialization_test.js similarity index 96% rename from tests/mocha/comment_deserialization_test.js rename to packages/blockly/tests/mocha/comment_deserialization_test.js index 2517ed77982..f834eb0f301 100644 --- a/tests/mocha/comment_deserialization_test.js +++ b/packages/blockly/tests/mocha/comment_deserialization_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, @@ -110,7 +110,7 @@ suite('Comment Deserialization', function () { test('Toolbox', function () { // Place from toolbox. const toolbox = this.workspace.getToolbox(); - simulateClick(toolbox.HtmlDiv.querySelector('.blocklyTreeRow')); + simulateClick(toolbox.HtmlDiv.querySelector('.blocklyToolboxCategory')); simulateClick( toolbox.getFlyout().svgGroup_.querySelector('.blocklyPath'), ); diff --git a/tests/mocha/comment_test.js b/packages/blockly/tests/mocha/comment_test.js similarity index 62% rename from tests/mocha/comment_test.js rename to packages/blockly/tests/mocha/comment_test.js index 79b3d7de662..a7b2635b99f 100644 --- a/tests/mocha/comment_test.js +++ b/packages/blockly/tests/mocha/comment_test.js @@ -5,7 +5,7 @@ */ import {EventType} from '../../build/src/core/events/type.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {assertEventFired} from './test_helpers/events.js'; import { sharedTestSetup, @@ -141,4 +141,75 @@ suite('Comments', function () { assertBubbleSize(this.comment, 100, 100); }); }); + suite('Set/Get Bubble Location', function () { + teardown(function () { + sinon.restore(); + }); + function assertBubbleLocation(comment, x, y) { + const location = comment.getBubbleLocation(); + assert.equal(location.x, x); + assert.equal(location.y, y); + } + test('Set Location While Visible', function () { + this.comment.setBubbleVisible(true); + + this.comment.setBubbleLocation(new Blockly.utils.Coordinate(100, 100)); + assertBubbleLocation(this.comment, 100, 100); + + this.comment.setBubbleVisible(false); + assertBubbleLocation(this.comment, 100, 100); + }); + test('Set Location While Invisible', function () { + this.comment.setBubbleLocation(new Blockly.utils.Coordinate(100, 100)); + assertBubbleLocation(this.comment, 100, 100); + + this.comment.setBubbleVisible(true); + assertBubbleLocation(this.comment, 100, 100); + }); + }); + suite('Undo/Redo', function () { + test('Adding an empty comment can be undone', function () { + const block = this.workspace.newBlock('empty_block'); + block.setCommentText(''); + assert.isNotNull(block.getIcon(Blockly.icons.IconType.COMMENT)); + assert.equal(block.getCommentText(), ''); + + this.workspace.undo(false); + + assert.isUndefined(block.getIcon(Blockly.icons.IconType.COMMENT)); + assert.isNull(block.getCommentText()); + }); + + test('Adding an empty comment can be redone', function () { + const block = this.workspace.newBlock('empty_block'); + block.setCommentText(''); + this.workspace.undo(false); + this.workspace.undo(true); + + assert.isNotNull(block.getIcon(Blockly.icons.IconType.COMMENT)); + assert.equal(block.getCommentText(), ''); + }); + + test('Adding a non-empty comment can be undone', function () { + const block = this.workspace.newBlock('empty_block'); + block.setCommentText('hey there'); + assert.isNotNull(block.getIcon(Blockly.icons.IconType.COMMENT)); + assert.equal(block.getCommentText(), 'hey there'); + + this.workspace.undo(false); + + assert.isUndefined(block.getIcon(Blockly.icons.IconType.COMMENT)); + assert.isNull(block.getCommentText()); + }); + + test('Adding a non-empty comment can be redone', function () { + const block = this.workspace.newBlock('empty_block'); + block.setCommentText('hey there'); + this.workspace.undo(false); + this.workspace.undo(true); + + assert.isNotNull(block.getIcon(Blockly.icons.IconType.COMMENT)); + assert.equal(block.getCommentText(), 'hey there'); + }); + }); }); diff --git a/tests/mocha/comment_view_test.js b/packages/blockly/tests/mocha/comment_view_test.js similarity index 99% rename from tests/mocha/comment_view_test.js rename to packages/blockly/tests/mocha/comment_view_test.js index 57a24742457..a60a7a973ff 100644 --- a/tests/mocha/comment_view_test.js +++ b/packages/blockly/tests/mocha/comment_view_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/connection_checker_test.js b/packages/blockly/tests/mocha/connection_checker_test.js similarity index 94% rename from tests/mocha/connection_checker_test.js rename to packages/blockly/tests/mocha/connection_checker_test.js index f353a2b77c2..bdbcb70a6ec 100644 --- a/tests/mocha/connection_checker_test.js +++ b/packages/blockly/tests/mocha/connection_checker_test.js @@ -5,7 +5,7 @@ */ import {ConnectionType} from '../../build/src/core/connection_type.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, @@ -29,7 +29,10 @@ suite('Connection checker', function () { } test('Target Null', function () { - const connection = new Blockly.Connection({}, ConnectionType.INPUT_VALUE); + const connection = new Blockly.Connection( + {id: 'test'}, + ConnectionType.INPUT_VALUE, + ); assertReasonHelper( this.checker, connection, @@ -38,7 +41,7 @@ suite('Connection checker', function () { ); }); test('Target Self', function () { - const block = {workspace: 1}; + const block = {id: 'test', workspace: 1}; const connection1 = new Blockly.Connection( block, ConnectionType.INPUT_VALUE, @@ -57,11 +60,11 @@ suite('Connection checker', function () { }); test('Different Workspaces', function () { const connection1 = new Blockly.Connection( - {workspace: 1}, + {id: 'test1', workspace: 1}, ConnectionType.INPUT_VALUE, ); const connection2 = new Blockly.Connection( - {workspace: 2}, + {id: 'test2', workspace: 2}, ConnectionType.OUTPUT_VALUE, ); @@ -76,10 +79,10 @@ suite('Connection checker', function () { setup(function () { // We have to declare each separately so that the connections belong // on different blocks. - const prevBlock = {isShadow: function () {}}; - const nextBlock = {isShadow: function () {}}; - const outBlock = {isShadow: function () {}}; - const inBlock = {isShadow: function () {}}; + const prevBlock = {id: 'test1', isShadow: function () {}}; + const nextBlock = {id: 'test2', isShadow: function () {}}; + const outBlock = {id: 'test3', isShadow: function () {}}; + const inBlock = {id: 'test4', isShadow: function () {}}; this.previous = new Blockly.Connection( prevBlock, ConnectionType.PREVIOUS_STATEMENT, @@ -197,11 +200,13 @@ suite('Connection checker', function () { suite('Shadows', function () { test('Previous Shadow', function () { const prevBlock = { + id: 'test1', isShadow: function () { return true; }, }; const nextBlock = { + id: 'test2', isShadow: function () { return false; }, @@ -224,11 +229,13 @@ suite('Connection checker', function () { }); test('Next Shadow', function () { const prevBlock = { + id: 'test1', isShadow: function () { return false; }, }; const nextBlock = { + id: 'test2', isShadow: function () { return true; }, @@ -251,11 +258,13 @@ suite('Connection checker', function () { }); test('Prev and Next Shadow', function () { const prevBlock = { + id: 'test1', isShadow: function () { return true; }, }; const nextBlock = { + id: 'test2', isShadow: function () { return true; }, @@ -278,11 +287,13 @@ suite('Connection checker', function () { }); test('Output Shadow', function () { const outBlock = { + id: 'test1', isShadow: function () { return true; }, }; const inBlock = { + id: 'test2', isShadow: function () { return false; }, @@ -305,11 +316,13 @@ suite('Connection checker', function () { }); test('Input Shadow', function () { const outBlock = { + id: 'test1', isShadow: function () { return false; }, }; const inBlock = { + id: 'test2', isShadow: function () { return true; }, @@ -332,11 +345,13 @@ suite('Connection checker', function () { }); test('Output and Input Shadow', function () { const outBlock = { + id: 'test1', isShadow: function () { return true; }, }; const inBlock = { + id: 'test2', isShadow: function () { return true; }, @@ -373,9 +388,11 @@ suite('Connection checker', function () { }; test('Output connected, adding previous', function () { const outBlock = { + id: 'test1', isShadow: function () {}, }; const inBlock = { + id: 'test2', isShadow: function () {}, }; const outCon = new Blockly.Connection( @@ -394,6 +411,7 @@ suite('Connection checker', function () { ConnectionType.PREVIOUS_STATEMENT, ); const nextBlock = { + id: 'test3', isShadow: function () {}, }; const nextCon = new Blockly.Connection( @@ -410,9 +428,11 @@ suite('Connection checker', function () { }); test('Previous connected, adding output', function () { const prevBlock = { + id: 'test1', isShadow: function () {}, }; const nextBlock = { + id: 'test2', isShadow: function () {}, }; const prevCon = new Blockly.Connection( @@ -431,6 +451,7 @@ suite('Connection checker', function () { ConnectionType.OUTPUT_VALUE, ); const inBlock = { + id: 'test3', isShadow: function () {}, }; const inCon = new Blockly.Connection( @@ -449,8 +470,14 @@ suite('Connection checker', function () { }); suite('Check Types', function () { setup(function () { - this.con1 = new Blockly.Connection({}, ConnectionType.PREVIOUS_STATEMENT); - this.con2 = new Blockly.Connection({}, ConnectionType.NEXT_STATEMENT); + this.con1 = new Blockly.Connection( + {id: 'test1'}, + ConnectionType.PREVIOUS_STATEMENT, + ); + this.con2 = new Blockly.Connection( + {id: 'test2'}, + ConnectionType.NEXT_STATEMENT, + ); }); function assertCheckTypes(checker, one, two) { assert.isTrue(checker.doTypeChecks(one, two)); diff --git a/tests/mocha/connection_db_test.js b/packages/blockly/tests/mocha/connection_db_test.js similarity index 98% rename from tests/mocha/connection_db_test.js rename to packages/blockly/tests/mocha/connection_db_test.js index e7f397d545f..459c59e3ab4 100644 --- a/tests/mocha/connection_db_test.js +++ b/packages/blockly/tests/mocha/connection_db_test.js @@ -5,7 +5,8 @@ */ import {ConnectionType} from '../../build/src/core/connection_type.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import * as idGenerator from '../../build/src/core/utils/idgenerator.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, @@ -31,7 +32,7 @@ suite('Connection Database', function () { }; workspace.connectionDBList[type] = opt_database || this.database; const connection = new Blockly.RenderedConnection( - {workspace: workspace}, + {id: idGenerator.getNextUniqueId(), workspace: workspace}, type, ); connection.x = x; diff --git a/tests/mocha/connection_test.js b/packages/blockly/tests/mocha/connection_test.js similarity index 99% rename from tests/mocha/connection_test.js rename to packages/blockly/tests/mocha/connection_test.js index cefea1784e7..b36f358eac3 100644 --- a/tests/mocha/connection_test.js +++ b/packages/blockly/tests/mocha/connection_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { defineRowBlock, defineStackBlock, diff --git a/tests/mocha/contextmenu_items_test.js b/packages/blockly/tests/mocha/contextmenu_items_test.js similarity index 75% rename from tests/mocha/contextmenu_items_test.js rename to packages/blockly/tests/mocha/contextmenu_items_test.js index a9e2bb3de62..52f4428ba7d 100644 --- a/tests/mocha/contextmenu_items_test.js +++ b/packages/blockly/tests/mocha/contextmenu_items_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, @@ -318,9 +318,7 @@ suite('Context Menu Items', function () { test('Deletes all blocks after confirming', function () { // Mocks the confirmation dialog and calls the callback with 'true' simulating ok. - const confirmStub = sinon - .stub(Blockly.dialog.TEST_ONLY, 'confirmInternal') - .callsArgWith(1, true); + const confirmStub = sinon.stub(window, 'confirm').returns(true); this.workspace.newBlock('text'); this.workspace.newBlock('text'); @@ -328,13 +326,13 @@ suite('Context Menu Items', function () { this.clock.runAll(); sinon.assert.calledOnce(confirmStub); assert.equal(this.workspace.getTopBlocks(false).length, 0); + + confirmStub.restore(); }); test('Does not delete blocks if not confirmed', function () { // Mocks the confirmation dialog and calls the callback with 'false' simulating cancel. - const confirmStub = sinon - .stub(Blockly.dialog.TEST_ONLY, 'confirmInternal') - .callsArgWith(1, false); + const confirmStub = sinon.stub(window, 'confirm').returns(false); this.workspace.newBlock('text'); this.workspace.newBlock('text'); @@ -342,19 +340,20 @@ suite('Context Menu Items', function () { this.clock.runAll(); sinon.assert.calledOnce(confirmStub); assert.equal(this.workspace.getTopBlocks(false).length, 2); + + confirmStub.restore(); }); test('No dialog for single block', function () { - const confirmStub = sinon.stub( - Blockly.dialog.TEST_ONLY, - 'confirmInternal', - ); + const confirmStub = sinon.stub(window, 'confirm'); this.workspace.newBlock('text'); this.deleteOption.callback(this.scope); this.clock.runAll(); sinon.assert.notCalled(confirmStub); assert.equal(this.workspace.getTopBlocks(false).length, 0); + + confirmStub.restore(); }); test('Has correct label for multiple blocks', function () { @@ -372,6 +371,89 @@ suite('Context Menu Items', function () { assert.equal(this.deleteOption.displayText(this.scope), 'Delete Block'); }); }); + + suite('Separators', function () { + setup(function () { + this.registry.reset(); + this.registry.register({ + weight: 0, + id: 'a', + preconditionFn: () => {}, + displayText: 'a', + callback: () => {}, + }); + this.registry.register({ + weight: 2, + id: 'b', + preconditionFn: () => {}, + displayText: 'b', + callback: () => {}, + }); + }); + + test('are hidden when precondition returns hidden', function () { + this.registry.register({ + weight: 1, + id: 'separator', + preconditionFn: () => 'hidden', + separator: true, + }); + + const items = this.registry.getContextMenuOptions( + this.scope, + new Event('pointerdown'), + ); + assert.equal(items.length, 2); + assert.isTrue(items.every((item) => !('separator' in item))); + }); + + test('are included when precondition returns enabled', function () { + this.registry.register({ + weight: 1, + id: 'separator', + preconditionFn: () => 'enabled', + separator: true, + }); + + const items = this.registry.getContextMenuOptions( + this.scope, + new Event('pointerdown'), + ); + assert.equal(items.length, 3); + assert.isTrue('separator' in items[1]); + }); + + test('are included when precondition returns disabled', function () { + this.registry.register({ + weight: 1, + id: 'separator', + preconditionFn: () => 'disabled', + separator: true, + }); + + const items = this.registry.getContextMenuOptions( + this.scope, + new Event('pointerdown'), + ); + assert.equal(items.length, 3); + assert.isTrue('separator' in items[1]); + }); + + test('are included when there is no precondition', function () { + this.registry.register({ + weight: 1, + id: 'separator', + separator: true, + }); + + const items = this.registry.getContextMenuOptions( + this.scope, + new Event('pointerdown'), + ); + assert.equal(items.length, 3); + assert.isTrue('separator' in items[1]); + }); + }); }); suite('Block Items', function () { @@ -484,5 +566,88 @@ suite('Context Menu Items', function () { assert.equal(this.inlineOption.preconditionFn(this.scope), 'enabled'); }); }); + + suite('Separators', function () { + setup(function () { + this.registry.reset(); + this.registry.register({ + weight: 0, + id: 'a', + preconditionFn: () => {}, + displayText: 'a', + callback: () => {}, + }); + this.registry.register({ + weight: 2, + id: 'b', + preconditionFn: () => {}, + displayText: 'b', + callback: () => {}, + }); + }); + + test('are hidden when precondition returns hidden', function () { + this.registry.register({ + weight: 1, + id: 'separator', + preconditionFn: () => 'hidden', + separator: true, + }); + + const items = this.registry.getContextMenuOptions( + this.scope, + new Event('pointerdown'), + ); + assert.equal(items.length, 2); + assert.isTrue(items.every((item) => !('separator' in item))); + }); + + test('are included when precondition returns enabled', function () { + this.registry.register({ + weight: 1, + id: 'separator', + preconditionFn: () => 'enabled', + separator: true, + }); + + const items = this.registry.getContextMenuOptions( + this.scope, + new Event('pointerdown'), + ); + assert.equal(items.length, 3); + assert.isTrue('separator' in items[1]); + }); + + test('are included when precondition returns disabled', function () { + this.registry.register({ + weight: 1, + id: 'separator', + preconditionFn: () => 'disabled', + separator: true, + }); + + const items = this.registry.getContextMenuOptions( + this.scope, + new Event('pointerdown'), + ); + assert.equal(items.length, 3); + assert.isTrue('separator' in items[1]); + }); + + test('are included when there is no precondition', function () { + this.registry.register({ + weight: 1, + id: 'separator', + separator: true, + }); + + const items = this.registry.getContextMenuOptions( + this.scope, + new Event('pointerdown'), + ); + assert.equal(items.length, 3); + assert.isTrue('separator' in items[1]); + }); + }); }); }); diff --git a/tests/mocha/contextmenu_test.js b/packages/blockly/tests/mocha/contextmenu_test.js similarity index 83% rename from tests/mocha/contextmenu_test.js rename to packages/blockly/tests/mocha/contextmenu_test.js index fe6d4be997e..df5bf79dc35 100644 --- a/tests/mocha/contextmenu_test.js +++ b/packages/blockly/tests/mocha/contextmenu_test.js @@ -6,8 +6,7 @@ import {callbackFactory} from '../../build/src/core/contextmenu.js'; import * as xmlUtils from '../../build/src/core/utils/xml.js'; -import * as Variables from '../../build/src/core/variables.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, @@ -32,9 +31,13 @@ suite('Context Menu', function () { }); test('callback with xml state creates block', function () { - const xmlField = Variables.generateVariableFieldDom( - this.forLoopBlock.getField('VAR').getVariable(), - ); + const variable = this.forLoopBlock.getField('VAR').getVariable(); + const xmlField = document.createElement('field'); + xmlField.setAttribute('name', 'VAR'); + xmlField.setAttribute('id', variable.getId()); + xmlField.setAttribute('variabletype', variable.getType()); + xmlField.textContent = variable.getName(); + const xmlBlock = xmlUtils.createElement('block'); xmlBlock.setAttribute('type', 'variables_get'); xmlBlock.appendChild(xmlField); diff --git a/packages/blockly/tests/mocha/cursor_test.js b/packages/blockly/tests/mocha/cursor_test.js new file mode 100644 index 00000000000..02426ae26b8 --- /dev/null +++ b/packages/blockly/tests/mocha/cursor_test.js @@ -0,0 +1,922 @@ +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {assert} from '../../node_modules/chai/index.js'; +import {createRenderedBlock} from './test_helpers/block_definitions.js'; +import { + sharedTestSetup, + sharedTestTeardown, +} from './test_helpers/setup_teardown.js'; + +suite('Cursor', function () { + suite('Movement', function () { + setup(function () { + sharedTestSetup.call(this); + Blockly.defineBlocksWithJsonArray([ + { + 'type': 'input_statement', + 'message0': '%1 %2 %3 %4', + 'args0': [ + { + 'type': 'field_input', + 'name': 'NAME1', + 'text': 'default', + }, + { + 'type': 'field_input', + 'name': 'NAME2', + 'text': 'default', + }, + { + 'type': 'input_value', + 'name': 'NAME3', + }, + { + 'type': 'input_statement', + 'name': 'NAME4', + }, + ], + 'previousStatement': null, + 'nextStatement': null, + 'colour': 230, + 'tooltip': '', + 'helpUrl': '', + }, + { + 'type': 'field_input', + 'message0': '%1', + 'args0': [ + { + 'type': 'field_input', + 'name': 'NAME', + 'text': 'default', + }, + ], + 'output': null, + 'colour': 230, + 'tooltip': '', + 'helpUrl': '', + }, + { + 'type': 'multi_statement_input', + 'message0': '%1 %2', + 'args0': [ + { + 'type': 'input_statement', + 'name': 'FIRST', + }, + { + 'type': 'input_statement', + 'name': 'SECOND', + }, + ], + }, + { + 'type': 'simple_statement', + 'message0': '%1', + 'args0': [ + { + 'type': 'field_input', + 'name': 'NAME', + 'text': 'default', + }, + ], + 'previousStatement': null, + 'nextStatement': null, + }, + ]); + this.workspace = Blockly.inject('blocklyDiv', {}); + this.cursor = this.workspace.getCursor(); + const blockA = createRenderedBlock(this.workspace, 'input_statement'); + const blockB = createRenderedBlock(this.workspace, 'input_statement'); + const blockC = createRenderedBlock(this.workspace, 'input_statement'); + const blockD = createRenderedBlock(this.workspace, 'input_statement'); + const blockE = createRenderedBlock(this.workspace, 'field_input'); + + blockA.nextConnection.connect(blockB.previousConnection); + blockA.inputList[0].connection.connect(blockE.outputConnection); + blockB.inputList[1].connection.connect(blockC.previousConnection); + this.cursor.drawer = null; + this.blocks = { + A: blockA, + B: blockB, + C: blockC, + D: blockD, + E: blockE, + }; + }); + teardown(function () { + sharedTestTeardown.call(this); + }); + + test('Next - From a Previous connection go to the next block', function () { + const prevNode = this.blocks.A.previousConnection; + this.cursor.setCurNode(prevNode); + this.cursor.next(); + const curNode = this.cursor.getCurNode(); + assert.equal(curNode, this.blocks.A); + }); + test('Next - From a block go to its statement input', function () { + const prevNode = this.blocks.B; + this.cursor.setCurNode(prevNode); + this.cursor.next(); + const curNode = this.cursor.getCurNode(); + assert.equal(curNode, this.blocks.C); + }); + + test('In - From field to attached input connection', function () { + const fieldBlock = this.blocks.E; + const fieldNode = this.blocks.A.getField('NAME2'); + this.cursor.setCurNode(fieldNode); + this.cursor.in(); + const curNode = this.cursor.getCurNode(); + assert.equal(curNode, fieldBlock); + }); + + test('Prev - From previous connection does skip over next connection', function () { + const prevConnection = this.blocks.B.previousConnection; + const prevConnectionNode = prevConnection; + this.cursor.setCurNode(prevConnectionNode); + this.cursor.prev(); + const curNode = this.cursor.getCurNode(); + assert.equal(curNode, this.blocks.A); + }); + + test('Prev - From first block loop to last block', function () { + const prevConnection = this.blocks.A; + const prevConnectionNode = prevConnection; + this.cursor.setCurNode(prevConnectionNode); + this.cursor.prev(); + const curNode = this.cursor.getCurNode(); + assert.equal(curNode, this.blocks.D); + }); + + test('Out - From field does not skip over block node', function () { + const field = this.blocks.E.inputList[0].fieldRow[0]; + const fieldNode = field; + this.cursor.setCurNode(fieldNode); + this.cursor.out(); + const curNode = this.cursor.getCurNode(); + assert.equal(curNode, this.blocks.E); + }); + + test('Out - From first connection loop to last next connection', function () { + const prevConnection = this.blocks.A.previousConnection; + const prevConnectionNode = prevConnection; + this.cursor.setCurNode(prevConnectionNode); + this.cursor.out(); + const curNode = this.cursor.getCurNode(); + assert.equal(curNode, this.blocks.D.nextConnection); + }); + }); + + suite('Multiple statement inputs', function () { + setup(function () { + sharedTestSetup.call(this); + Blockly.defineBlocksWithJsonArray([ + { + 'type': 'multi_statement_input', + 'message0': '%1 %2', + 'args0': [ + { + 'type': 'input_statement', + 'name': 'FIRST', + }, + { + 'type': 'input_statement', + 'name': 'SECOND', + }, + ], + }, + { + 'type': 'simple_statement', + 'message0': '%1', + 'args0': [ + { + 'type': 'field_input', + 'name': 'NAME', + 'text': 'default', + }, + ], + 'previousStatement': null, + 'nextStatement': null, + }, + ]); + this.workspace = Blockly.inject('blocklyDiv', {}); + this.cursor = this.workspace.getCursor(); + + this.multiStatement1 = createRenderedBlock( + this.workspace, + 'multi_statement_input', + ); + this.multiStatement2 = createRenderedBlock( + this.workspace, + 'multi_statement_input', + ); + this.firstStatement = createRenderedBlock( + this.workspace, + 'simple_statement', + ); + this.secondStatement = createRenderedBlock( + this.workspace, + 'simple_statement', + ); + this.thirdStatement = createRenderedBlock( + this.workspace, + 'simple_statement', + ); + this.fourthStatement = createRenderedBlock( + this.workspace, + 'simple_statement', + ); + this.multiStatement1 + .getInput('FIRST') + .connection.connect(this.firstStatement.previousConnection); + this.firstStatement.nextConnection.connect( + this.secondStatement.previousConnection, + ); + this.multiStatement1 + .getInput('SECOND') + .connection.connect(this.thirdStatement.previousConnection); + this.multiStatement2 + .getInput('FIRST') + .connection.connect(this.fourthStatement.previousConnection); + }); + + teardown(function () { + sharedTestTeardown.call(this); + }); + + test('In - from field in nested statement block to next nested statement block', function () { + this.cursor.setCurNode(this.secondStatement.getField('NAME')); + this.cursor.in(); + const curNode = this.cursor.getCurNode(); + assert.equal(curNode, this.thirdStatement); + }); + test('In - from field in nested statement block to next stack', function () { + this.cursor.setCurNode(this.thirdStatement.getField('NAME')); + this.cursor.in(); + const curNode = this.cursor.getCurNode(); + assert.equal(curNode, this.multiStatement2); + }); + + test('Out - from nested statement block to last field of previous nested statement block', function () { + this.cursor.setCurNode(this.thirdStatement); + this.cursor.out(); + const curNode = this.cursor.getCurNode(); + assert.equal(curNode, this.secondStatement.getField('NAME')); + }); + + test('Out - from root block to last field of last nested statement block in previous stack', function () { + this.cursor.setCurNode(this.multiStatement2); + this.cursor.out(); + const curNode = this.cursor.getCurNode(); + assert.equal(curNode, this.thirdStatement.getField('NAME')); + }); + }); + + suite('Searching', function () { + setup(function () { + sharedTestSetup.call(this); + Blockly.defineBlocksWithJsonArray([ + { + 'type': 'empty_block', + 'message0': '', + }, + { + 'type': 'stack_block', + 'message0': '', + 'previousStatement': null, + 'nextStatement': null, + }, + { + 'type': 'row_block', + 'message0': '%1', + 'args0': [ + { + 'type': 'input_value', + 'name': 'INPUT', + }, + ], + 'output': null, + }, + { + 'type': 'statement_block', + 'message0': '%1', + 'args0': [ + { + 'type': 'input_statement', + 'name': 'STATEMENT', + }, + ], + 'previousStatement': null, + 'nextStatement': null, + }, + { + 'type': 'c_hat_block', + 'message0': '%1', + 'args0': [ + { + 'type': 'input_statement', + 'name': 'STATEMENT', + }, + ], + }, + ]); + this.workspace = Blockly.inject('blocklyDiv', {}); + this.cursor = this.workspace.getCursor(); + }); + teardown(function () { + sharedTestTeardown.call(this); + }); + suite('one empty block', function () { + setup(function () { + this.blockA = this.workspace.newBlock('empty_block'); + }); + teardown(function () { + this.workspace.clear(); + }); + test('getFirstNode', function () { + const node = this.cursor.getFirstNode(); + assert.equal(node, this.blockA); + }); + test('getLastNode', function () { + const node = this.cursor.getLastNode(); + assert.equal(node, this.blockA); + }); + }); + + suite('one stack block', function () { + setup(function () { + this.blockA = this.workspace.newBlock('stack_block'); + }); + teardown(function () { + this.workspace.clear(); + }); + test('getFirstNode', function () { + const node = this.cursor.getFirstNode(); + assert.equal(node, this.blockA); + }); + test('getLastNode', function () { + const node = this.cursor.getLastNode(); + assert.equal(node, this.blockA); + }); + }); + + suite('one row block', function () { + setup(function () { + this.blockA = this.workspace.newBlock('row_block'); + }); + teardown(function () { + this.workspace.clear(); + }); + test('getFirstNode', function () { + const node = this.cursor.getFirstNode(); + assert.equal(node, this.blockA); + }); + test('getLastNode', function () { + const node = this.cursor.getLastNode(); + assert.equal(node, this.blockA.inputList[0].connection); + }); + }); + suite('one c-hat block', function () { + setup(function () { + this.blockA = this.workspace.newBlock('c_hat_block'); + }); + teardown(function () { + this.workspace.clear(); + }); + test('getFirstNode', function () { + const node = this.cursor.getFirstNode(); + assert.equal(node, this.blockA); + }); + test('getLastNode', function () { + const node = this.cursor.getLastNode(); + assert.equal(node, this.blockA); + }); + }); + + suite('multiblock stack', function () { + setup(function () { + const state = { + 'blocks': { + 'languageVersion': 0, + 'blocks': [ + { + 'type': 'stack_block', + 'id': 'A', + 'x': 0, + 'y': 0, + 'next': { + 'block': { + 'type': 'stack_block', + 'id': 'B', + }, + }, + }, + ], + }, + }; + Blockly.serialization.workspaces.load(state, this.workspace); + }); + teardown(function () { + this.workspace.clear(); + }); + test('getFirstNode', function () { + const node = this.cursor.getFirstNode(); + const blockA = this.workspace.getBlockById('A'); + assert.equal(node, blockA); + }); + test('getLastNode', function () { + const node = this.cursor.getLastNode(); + const blockB = this.workspace.getBlockById('B'); + assert.equal(node, blockB); + }); + }); + + suite('multiblock row', function () { + setup(function () { + const state = { + 'blocks': { + 'languageVersion': 0, + 'blocks': [ + { + 'type': 'row_block', + 'id': 'A', + 'x': 0, + 'y': 0, + 'inputs': { + 'INPUT': { + 'block': { + 'type': 'row_block', + 'id': 'B', + }, + }, + }, + }, + ], + }, + }; + Blockly.serialization.workspaces.load(state, this.workspace); + }); + teardown(function () { + this.workspace.clear(); + }); + test('getFirstNode', function () { + const node = this.cursor.getFirstNode(); + const blockA = this.workspace.getBlockById('A'); + assert.equal(node, blockA); + }); + test('getLastNode', function () { + const node = this.cursor.getLastNode(); + const blockB = this.workspace.getBlockById('B'); + assert.equal(node, blockB.inputList[0].connection); + }); + }); + + suite('two stacks', function () { + setup(function () { + const state = { + 'blocks': { + 'languageVersion': 0, + 'blocks': [ + { + 'type': 'stack_block', + 'id': 'A', + 'x': 0, + 'y': 0, + 'next': { + 'block': { + 'type': 'stack_block', + 'id': 'B', + }, + }, + }, + { + 'type': 'stack_block', + 'id': 'C', + 'x': 100, + 'y': 100, + 'next': { + 'block': { + 'type': 'stack_block', + 'id': 'D', + }, + }, + }, + ], + }, + }; + Blockly.serialization.workspaces.load(state, this.workspace); + }); + teardown(function () { + this.workspace.clear(); + }); + test('getFirstNode', function () { + const node = this.cursor.getFirstNode(); + const location = node; + const blockA = this.workspace.getBlockById('A'); + assert.equal(location, blockA); + }); + test('getLastNode', function () { + const node = this.cursor.getLastNode(); + const location = node; + const blockD = this.workspace.getBlockById('D'); + assert.equal(location, blockD); + }); + }); + }); + suite('Get next node', function () { + setup(function () { + sharedTestSetup.call(this); + Blockly.defineBlocksWithJsonArray([ + { + 'type': 'empty_block', + 'message0': '', + }, + { + 'type': 'stack_block', + 'message0': '%1', + 'args0': [ + { + 'type': 'field_input', + 'name': 'FIELD', + 'text': 'default', + }, + ], + 'previousStatement': null, + 'nextStatement': null, + }, + { + 'type': 'row_block', + 'message0': '%1 %2', + 'args0': [ + { + 'type': 'field_input', + 'name': 'FIELD', + 'text': 'default', + }, + { + 'type': 'input_value', + 'name': 'INPUT', + }, + ], + 'output': null, + }, + ]); + this.workspace = Blockly.inject('blocklyDiv', {}); + this.cursor = this.workspace.getCursor(); + this.neverValid = () => false; + this.alwaysValid = () => true; + this.isBlock = (node) => { + return node && node instanceof Blockly.BlockSvg; + }; + }); + teardown(function () { + sharedTestTeardown.call(this); + }); + suite('stack', function () { + setup(function () { + const state = { + 'blocks': { + 'languageVersion': 0, + 'blocks': [ + { + 'type': 'stack_block', + 'id': 'A', + 'x': 0, + 'y': 0, + 'next': { + 'block': { + 'type': 'stack_block', + 'id': 'B', + 'next': { + 'block': { + 'type': 'stack_block', + 'id': 'C', + }, + }, + }, + }, + }, + ], + }, + }; + Blockly.serialization.workspaces.load(state, this.workspace); + this.blockA = this.workspace.getBlockById('A'); + this.blockB = this.workspace.getBlockById('B'); + this.blockC = this.workspace.getBlockById('C'); + }); + teardown(function () { + this.workspace.clear(); + }); + test('Never valid - start at top', function () { + const startNode = this.blockA.previousConnection; + const nextNode = this.cursor.getNextNode( + startNode, + this.neverValid, + false, + ); + assert.isNull(nextNode); + }); + test('Never valid - start in middle', function () { + const startNode = this.blockB; + const nextNode = this.cursor.getNextNode( + startNode, + this.neverValid, + false, + ); + assert.isNull(nextNode); + }); + test('Never valid - start at end', function () { + const startNode = this.blockC.nextConnection; + const nextNode = this.cursor.getNextNode( + startNode, + this.neverValid, + false, + ); + assert.isNull(nextNode); + }); + + test('Always valid - start at top', function () { + const startNode = this.blockA.previousConnection; + const nextNode = this.cursor.getNextNode( + startNode, + this.alwaysValid, + false, + ); + assert.equal(nextNode, this.blockA); + }); + test('Always valid - start in middle', function () { + const startNode = this.blockB; + const nextNode = this.cursor.getNextNode( + startNode, + this.alwaysValid, + false, + ); + assert.equal(nextNode, this.blockB.getField('FIELD')); + }); + test('Always valid - start at end', function () { + const startNode = this.blockC.getField('FIELD'); + const nextNode = this.cursor.getNextNode( + startNode, + this.alwaysValid, + false, + ); + assert.isNull(nextNode); + }); + + test('Valid if block - start at top', function () { + const startNode = this.blockA; + const nextNode = this.cursor.getNextNode( + startNode, + this.isBlock, + false, + ); + assert.equal(nextNode, this.blockB); + }); + test('Valid if block - start in middle', function () { + const startNode = this.blockB; + const nextNode = this.cursor.getNextNode( + startNode, + this.isBlock, + false, + ); + assert.equal(nextNode, this.blockC); + }); + test('Valid if block - start at end', function () { + const startNode = this.blockC.getField('FIELD'); + const nextNode = this.cursor.getNextNode( + startNode, + this.isBlock, + false, + ); + assert.isNull(nextNode); + }); + test('Never valid - start at end - with loopback', function () { + const startNode = this.blockC.nextConnection; + const nextNode = this.cursor.getNextNode( + startNode, + this.neverValid, + true, + ); + assert.isNull(nextNode); + }); + test('Always valid - start at end - with loopback', function () { + const startNode = this.blockC.nextConnection; + const nextNode = this.cursor.getNextNode( + startNode, + this.alwaysValid, + true, + ); + assert.equal(nextNode, this.blockA.previousConnection); + }); + + test('Valid if block - start at end - with loopback', function () { + const startNode = this.blockC; + const nextNode = this.cursor.getNextNode(startNode, this.isBlock, true); + assert.equal(nextNode, this.blockA); + }); + }); + }); + + suite('Get previous node', function () { + setup(function () { + sharedTestSetup.call(this); + Blockly.defineBlocksWithJsonArray([ + { + 'type': 'empty_block', + 'message0': '', + }, + { + 'type': 'stack_block', + 'message0': '%1', + 'args0': [ + { + 'type': 'field_input', + 'name': 'FIELD', + 'text': 'default', + }, + ], + 'previousStatement': null, + 'nextStatement': null, + }, + { + 'type': 'row_block', + 'message0': '%1 %2', + 'args0': [ + { + 'type': 'field_input', + 'name': 'FIELD', + 'text': 'default', + }, + { + 'type': 'input_value', + 'name': 'INPUT', + }, + ], + 'output': null, + }, + ]); + this.workspace = Blockly.inject('blocklyDiv', {}); + this.cursor = this.workspace.getCursor(); + this.neverValid = () => false; + this.alwaysValid = () => true; + this.isBlock = (node) => { + return node && node instanceof Blockly.BlockSvg; + }; + }); + teardown(function () { + sharedTestTeardown.call(this); + }); + suite('stack', function () { + setup(function () { + const state = { + 'blocks': { + 'languageVersion': 0, + 'blocks': [ + { + 'type': 'stack_block', + 'id': 'A', + 'x': 0, + 'y': 0, + 'next': { + 'block': { + 'type': 'stack_block', + 'id': 'B', + 'next': { + 'block': { + 'type': 'stack_block', + 'id': 'C', + }, + }, + }, + }, + }, + ], + }, + }; + Blockly.serialization.workspaces.load(state, this.workspace); + this.blockA = this.workspace.getBlockById('A'); + this.blockB = this.workspace.getBlockById('B'); + this.blockC = this.workspace.getBlockById('C'); + }); + teardown(function () { + this.workspace.clear(); + }); + test('Never valid - start at top', function () { + const startNode = this.blockA.previousConnection; + const previousNode = this.cursor.getPreviousNode( + startNode, + this.neverValid, + false, + ); + assert.isNull(previousNode); + }); + test('Never valid - start in middle', function () { + const startNode = this.blockB; + const previousNode = this.cursor.getPreviousNode( + startNode, + this.neverValid, + false, + ); + assert.isNull(previousNode); + }); + test('Never valid - start at end', function () { + const startNode = this.blockC.nextConnection; + const previousNode = this.cursor.getPreviousNode( + startNode, + this.neverValid, + false, + ); + assert.isNull(previousNode); + }); + + test('Always valid - start at top', function () { + const startNode = this.blockA; + const previousNode = this.cursor.getPreviousNode( + startNode, + this.alwaysValid, + false, + ); + assert.isNull(previousNode); + }); + test('Always valid - start in middle', function () { + const startNode = this.blockB; + const previousNode = this.cursor.getPreviousNode( + startNode, + this.alwaysValid, + false, + ); + assert.equal(previousNode, this.blockA.getField('FIELD')); + }); + test('Always valid - start at end', function () { + const startNode = this.blockC.nextConnection; + const previousNode = this.cursor.getPreviousNode( + startNode, + this.alwaysValid, + false, + ); + assert.equal(previousNode, this.blockC.getField('FIELD')); + }); + + test('Valid if block - start at top', function () { + const startNode = this.blockA; + const previousNode = this.cursor.getPreviousNode( + startNode, + this.isBlock, + false, + ); + assert.isNull(previousNode); + }); + test('Valid if block - start in middle', function () { + const startNode = this.blockB; + const previousNode = this.cursor.getPreviousNode( + startNode, + this.isBlock, + false, + ); + assert.equal(previousNode, this.blockA); + }); + test('Valid if block - start at end', function () { + const startNode = this.blockC; + const previousNode = this.cursor.getPreviousNode( + startNode, + this.isBlock, + false, + ); + assert.equal(previousNode, this.blockB); + }); + test('Never valid - start at top - with loopback', function () { + const startNode = this.blockA.previousConnection; + const previousNode = this.cursor.getPreviousNode( + startNode, + this.neverValid, + true, + ); + assert.isNull(previousNode); + }); + test('Always valid - start at top - with loopback', function () { + const startNode = this.blockA.previousConnection; + const previousNode = this.cursor.getPreviousNode( + startNode, + this.alwaysValid, + true, + ); + assert.equal(previousNode, this.blockC.nextConnection); + }); + test('Valid if block - start at top - with loopback', function () { + const startNode = this.blockA; + const previousNode = this.cursor.getPreviousNode( + startNode, + this.isBlock, + true, + ); + assert.equal(previousNode, this.blockC); + }); + }); + }); +}); diff --git a/packages/blockly/tests/mocha/dialog_test.js b/packages/blockly/tests/mocha/dialog_test.js new file mode 100644 index 00000000000..7d4147d83f8 --- /dev/null +++ b/packages/blockly/tests/mocha/dialog_test.js @@ -0,0 +1,168 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {assert} from '../../node_modules/chai/index.js'; +import { + sharedTestSetup, + sharedTestTeardown, +} from './test_helpers/setup_teardown.js'; + +suite('Dialog utilities', function () { + setup(function () { + sharedTestSetup.call(this); + this.workspace = Blockly.inject('blocklyDiv', {}); + }); + + teardown(function () { + sharedTestTeardown.call(this); + Blockly.dialog.setAlert(); + Blockly.dialog.setPrompt(); + Blockly.dialog.setConfirm(); + Blockly.dialog.setToast(); + }); + + test('use the browser alert by default', function () { + const alert = sinon.stub(window, 'alert'); + Blockly.dialog.alert('test'); + assert.isTrue(alert.calledWith('test')); + alert.restore(); + }); + + test('support setting a custom alert handler', function () { + const alert = sinon.spy(); + Blockly.dialog.setAlert(alert); + const callback = () => {}; + const message = 'test'; + Blockly.dialog.alert(message, callback); + assert.isTrue(alert.calledWith('test', callback)); + }); + + test('do not call the browser alert if a custom alert handler is set', function () { + const browserAlert = sinon.stub(window, 'alert'); + + const alert = sinon.spy(); + Blockly.dialog.setAlert(alert); + Blockly.dialog.alert(test); + assert.isFalse(browserAlert.called); + + browserAlert.restore(); + }); + + test('use the browser confirm by default', function () { + const confirm = sinon.stub(window, 'confirm'); + const callback = () => {}; + const message = 'test'; + Blockly.dialog.confirm(message, callback); + assert.isTrue(confirm.calledWith(message)); + confirm.restore(); + }); + + test('support setting a custom confirm handler', function () { + const confirm = sinon.spy(); + Blockly.dialog.setConfirm(confirm); + const callback = () => {}; + const message = 'test'; + Blockly.dialog.confirm(message, callback); + assert.isTrue(confirm.calledWith('test', callback)); + }); + + test('do not call the browser confirm if a custom confirm handler is set', function () { + const browserConfirm = sinon.stub(window, 'confirm'); + + const confirm = sinon.spy(); + Blockly.dialog.setConfirm(confirm); + const callback = () => {}; + const message = 'test'; + Blockly.dialog.confirm(message, callback); + assert.isFalse(browserConfirm.called); + + browserConfirm.restore(); + }); + + test('invokes the provided callback with the confirmation response', function () { + const confirm = sinon.stub(window, 'confirm').returns(true); + const callback = sinon.spy(); + const message = 'test'; + Blockly.dialog.confirm(message, callback); + assert.isTrue(callback.calledWith(true)); + confirm.restore(); + }); + + test('use the browser prompt by default', function () { + const prompt = sinon.stub(window, 'prompt'); + const callback = () => {}; + const message = 'test'; + const defaultValue = 'default'; + Blockly.dialog.prompt(message, defaultValue, callback); + assert.isTrue(prompt.calledWith(message, defaultValue)); + prompt.restore(); + }); + + test('support setting a custom prompt handler', function () { + const prompt = sinon.spy(); + Blockly.dialog.setPrompt(prompt); + const callback = () => {}; + const message = 'test'; + const defaultValue = 'default'; + Blockly.dialog.prompt(message, defaultValue, callback); + assert.isTrue(prompt.calledWith('test', defaultValue, callback)); + }); + + test('do not call the browser prompt if a custom prompt handler is set', function () { + const browserPrompt = sinon.stub(window, 'prompt'); + + const prompt = sinon.spy(); + Blockly.dialog.setPrompt(prompt); + const callback = () => {}; + const message = 'test'; + const defaultValue = 'default'; + Blockly.dialog.prompt(message, defaultValue, callback); + assert.isFalse(browserPrompt.called); + + browserPrompt.restore(); + }); + + test('invokes the provided callback with the prompt response', function () { + const prompt = sinon.stub(window, 'prompt').returns('something'); + const callback = sinon.spy(); + const message = 'test'; + const defaultValue = 'default'; + Blockly.dialog.prompt(message, defaultValue, callback); + assert.isTrue(callback.calledWith('something')); + prompt.restore(); + }); + + test('use the built-in toast by default', function () { + const message = 'test toast'; + Blockly.dialog.toast(this.workspace, {message}); + const toast = this.workspace + .getInjectionDiv() + .querySelector('.blocklyToast'); + assert.isNotNull(toast); + assert.equal(toast.textContent, message); + }); + + test('support setting a custom toast handler', function () { + const toast = sinon.spy(); + Blockly.dialog.setToast(toast); + const message = 'test toast'; + const options = {message}; + Blockly.dialog.toast(this.workspace, options); + assert.isTrue(toast.calledWith(this.workspace, options)); + }); + + test('do not use the built-in toast if a custom toast handler is set', function () { + const builtInToast = sinon.stub(Blockly.Toast, 'show'); + + const toast = sinon.spy(); + Blockly.dialog.setToast(toast); + const message = 'test toast'; + Blockly.dialog.toast(this.workspace, {message}); + assert.isFalse(builtInToast.called); + + builtInToast.restore(); + }); +}); diff --git a/packages/blockly/tests/mocha/dropdowndiv_test.js b/packages/blockly/tests/mocha/dropdowndiv_test.js new file mode 100644 index 00000000000..495237f18bc --- /dev/null +++ b/packages/blockly/tests/mocha/dropdowndiv_test.js @@ -0,0 +1,458 @@ +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {Rect} from '../../build/src/core/utils/rect.js'; +import * as style from '../../build/src/core/utils/style.js'; +import {assert} from '../../node_modules/chai/index.js'; +import { + sharedTestSetup, + sharedTestTeardown, +} from './test_helpers/setup_teardown.js'; + +suite('DropDownDiv', function () { + setup(function () { + sharedTestSetup.call(this); + this.workspace = Blockly.inject('blocklyDiv'); + this.setUpBlockWithField = function () { + const blockJson = { + 'type': 'text', + 'id': 'block_id', + 'x': 10, + 'y': 20, + 'fields': { + 'TEXT': '', + }, + }; + Blockly.serialization.blocks.append(blockJson, this.workspace); + return this.workspace.getBlockById('block_id'); + }; + // The workspace needs to be visible for focus-specific tests. + document.getElementById('blocklyDiv').style.visibility = 'visible'; + }); + teardown(function () { + sharedTestTeardown.call(this); + document.getElementById('blocklyDiv').style.visibility = 'hidden'; + }); + + suite('Positioning', function () { + setup(function () { + this.boundsStub = sinon + .stub(Blockly.DropDownDiv.TEST_ONLY, 'getBoundsInfo') + .returns({ + left: 0, + right: 100, + top: 0, + bottom: 100, + width: 100, + height: 100, + }); + this.sizeStub = sinon + .stub(Blockly.utils.style.TEST_ONLY, 'getSizeInternal') + .returns({ + width: 60, + height: 60, + }); + this.clientHeightStub = sinon + .stub(document.documentElement, 'clientHeight') + .get(function () { + return 1000; + }); + this.clientTopStub = sinon + .stub(document.documentElement, 'clientTop') + .get(function () { + return 0; + }); + }); + test('Below, in Bounds', function () { + const metrics = Blockly.DropDownDiv.TEST_ONLY.getPositionMetrics( + 50, + 0, + 50, + -10, + ); + // "Above" in value actually means below in render. + assert.isAtLeast(metrics.initialY, 0); + assert.isAbove(metrics.finalY, 0); + assert.isTrue(metrics.arrowVisible); + assert.isTrue(metrics.arrowAtTop); + }); + test('Above, in Bounds', function () { + const metrics = Blockly.DropDownDiv.TEST_ONLY.getPositionMetrics( + 50, + 100, + 50, + 90, + ); + // "Below" in value actually means above in render. + assert.isAtMost(metrics.initialY, 100); + assert.isBelow(metrics.finalY, 100); + assert.isTrue(metrics.arrowVisible); + assert.isFalse(metrics.arrowAtTop); + }); + test('Below, out of Bounds', function () { + const metrics = Blockly.DropDownDiv.TEST_ONLY.getPositionMetrics( + 50, + 60, + 50, + 50, + ); + // "Above" in value actually means below in render. + assert.isAtLeast(metrics.initialY, 60); + assert.isAbove(metrics.finalY, 60); + assert.isTrue(metrics.arrowVisible); + assert.isTrue(metrics.arrowAtTop); + }); + test('Above, in Bounds', function () { + const metrics = Blockly.DropDownDiv.TEST_ONLY.getPositionMetrics( + 50, + 100, + 50, + 90, + ); + // "Below" in value actually means above in render. + assert.isAtMost(metrics.initialY, 100); + assert.isBelow(metrics.finalY, 100); + assert.isTrue(metrics.arrowVisible); + assert.isFalse(metrics.arrowAtTop); + }); + test('No Solution, Render At Top', function () { + this.clientHeightStub.get(function () { + return 100; + }); + const metrics = Blockly.DropDownDiv.TEST_ONLY.getPositionMetrics( + 50, + 60, + 50, + 50, + ); + // "Above" in value actually means below in render. + assert.equal(metrics.initialY, 0); + assert.equal(metrics.finalY, 0); + assert.isFalse(metrics.arrowVisible); + assert.isNotOk(metrics.arrowAtTop); + }); + }); + + suite('Keyboard Shortcuts', function () { + setup(function () { + this.boundsStub = sinon + .stub(Blockly.DropDownDiv.TEST_ONLY, 'getBoundsInfo') + .returns({ + left: 0, + right: 100, + top: 0, + bottom: 100, + width: 100, + height: 100, + }); + this.workspace = Blockly.inject('blocklyDiv', {}); + }); + teardown(function () { + this.boundsStub.restore(); + }); + test('Escape dismisses DropDownDiv', function () { + let hidden = false; + Blockly.DropDownDiv.show(this, false, 0, 0, 0, 0, false, () => { + hidden = true; + }); + assert.isFalse(hidden); + Blockly.DropDownDiv.getContentDiv().dispatchEvent( + new KeyboardEvent('keydown', { + key: 'Escape', + keyCode: 27, // example values. + }), + ); + assert.isTrue(hidden); + }); + }); + + suite('show()', function () { + test('without bounds set throws error', function () { + const block = this.setUpBlockWithField(); + const field = Array.from(block.getFields())[0]; + + const errorMsgRegex = /Cannot read properties of null.+?/; + assert.throws( + () => Blockly.DropDownDiv.show(field, false, 50, 60, 70, 80, false), + errorMsgRegex, + ); + }); + + test('with bounds set positions and shows div near specified location', function () { + Blockly.DropDownDiv.setBoundsElement(document.body); + const block = this.setUpBlockWithField(); + const field = Array.from(block.getFields())[0]; + + Blockly.DropDownDiv.show(field, false, 50, 60, 70, 80, false); + + const dropDownDivElem = document.querySelector('.blocklyDropDownDiv'); + assert.strictEqual(dropDownDivElem.style.opacity, '1'); + assert.strictEqual(dropDownDivElem.style.left, '45px'); + assert.strictEqual(dropDownDivElem.style.top, '60px'); + }); + }); + + suite('showPositionedByField()', function () { + test('shows div near field', function () { + const block = this.setUpBlockWithField(); + const field = Array.from(block.getFields())[0]; + const fieldBounds = field.getScaledBBox(); + + Blockly.DropDownDiv.showPositionedByField(field); + + // The div should show below the field and centered horizontally. + const dropDownDivElem = document.querySelector('.blocklyDropDownDiv'); + const divWidth = style.getSize(dropDownDivElem).width; + const expectedLeft = Math.floor( + fieldBounds.left + fieldBounds.getWidth() / 2 - divWidth / 2, + ); + const expectedTop = Math.floor(fieldBounds.bottom); // Should show beneath. + assert.strictEqual(dropDownDivElem.style.opacity, '1'); + assert.strictEqual(dropDownDivElem.style.left, `${expectedLeft}px`); + assert.strictEqual(dropDownDivElem.style.top, `${expectedTop}px`); + }); + + test('with hide callback does not call callback', function () { + const block = this.setUpBlockWithField(); + const field = Array.from(block.getFields())[0]; + const onHideCallback = sinon.stub(); + + Blockly.DropDownDiv.showPositionedByField(field, onHideCallback); + + // Simply showing the div should never call the hide callback. + assert.strictEqual(onHideCallback.callCount, 0); + }); + + test('without managed ephemeral focus does not change focused node', function () { + const block = this.setUpBlockWithField(); + const field = Array.from(block.getFields())[0]; + Blockly.getFocusManager().focusNode(block); + + Blockly.DropDownDiv.showPositionedByField(field, null, null, false); + + // Since managing ephemeral focus is disabled the current focused node shouldn't be changed. + const blockFocusableElem = block.getFocusableElement(); + assert.strictEqual(Blockly.getFocusManager().getFocusedNode(), block); + assert.strictEqual(document.activeElement, blockFocusableElem); + }); + + test('with managed ephemeral focus focuses drop-down div', function () { + const block = this.setUpBlockWithField(); + const field = Array.from(block.getFields())[0]; + Blockly.getFocusManager().focusNode(block); + + Blockly.DropDownDiv.showPositionedByField(field, null, null, true); + + // Managing ephemeral focus won't change getFocusedNode() but will change the actual element + // with DOM focus. + const dropDownDivElem = document.querySelector('.blocklyDropDownDiv'); + assert.strictEqual(Blockly.getFocusManager().getFocusedNode(), block); + assert.strictEqual(document.activeElement, dropDownDivElem); + }); + }); + + suite('showPositionedByBlock()', function () { + test('shows div near block', function () { + const block = this.setUpBlockWithField(); + const field = Array.from(block.getFields())[0]; + // Note that the offset must be computed before showing the div since otherwise it can move + // slightly after the div is shown. + const blockOffset = style.getPageOffset(block.getSvgRoot()); + + Blockly.DropDownDiv.showPositionedByBlock(field, block); + + // The div should show below the block and centered horizontally. + const blockLocalBounds = block.getBoundingRectangle(); + const blockBounds = Rect.createFromPoint( + blockOffset, + blockLocalBounds.getWidth(), + blockLocalBounds.getHeight(), + ); + const dropDownDivElem = document.querySelector('.blocklyDropDownDiv'); + const divWidth = style.getSize(dropDownDivElem).width; + const expectedLeft = Math.floor( + blockBounds.left + blockBounds.getWidth() / 2 - divWidth / 2, + ); + const expectedTop = Math.floor(blockBounds.bottom); // Should show beneath. + assert.strictEqual(dropDownDivElem.style.opacity, '1'); + assert.strictEqual(dropDownDivElem.style.left, `${expectedLeft}px`); + assert.strictEqual(dropDownDivElem.style.top, `${expectedTop}px`); + }); + + test('with hide callback does not call callback', function () { + const block = this.setUpBlockWithField(); + const field = Array.from(block.getFields())[0]; + const onHideCallback = sinon.stub(); + + Blockly.DropDownDiv.showPositionedByBlock(field, block, onHideCallback); + + // Simply showing the div should never call the hide callback. + assert.strictEqual(onHideCallback.callCount, 0); + }); + + test('without managed ephemeral focus does not change focused node', function () { + const block = this.setUpBlockWithField(); + const field = Array.from(block.getFields())[0]; + Blockly.getFocusManager().focusNode(block); + + Blockly.DropDownDiv.showPositionedByBlock( + field, + block, + null, + null, + false, + ); + + // Since managing ephemeral focus is disabled the current focused node shouldn't be changed. + const blockFocusableElem = block.getFocusableElement(); + assert.strictEqual(Blockly.getFocusManager().getFocusedNode(), block); + assert.strictEqual(document.activeElement, blockFocusableElem); + }); + + test('with managed ephemeral focus focuses drop-down div', function () { + const block = this.setUpBlockWithField(); + const field = Array.from(block.getFields())[0]; + Blockly.getFocusManager().focusNode(block); + + Blockly.DropDownDiv.showPositionedByBlock(field, block, null, null, true); + + // Managing ephemeral focus won't change getFocusedNode() but will change the actual element + // with DOM focus. + const dropDownDivElem = document.querySelector('.blocklyDropDownDiv'); + assert.strictEqual(Blockly.getFocusManager().getFocusedNode(), block); + assert.strictEqual(document.activeElement, dropDownDivElem); + }); + }); + + suite('hideWithoutAnimation()', function () { + test('when not showing drop-down div keeps opacity at 0', function () { + Blockly.DropDownDiv.hideWithoutAnimation(); + + const dropDownDivElem = document.querySelector('.blocklyDropDownDiv'); + assert.strictEqual(dropDownDivElem.style.opacity, '0'); + }); + + suite('for div positioned by field', function () { + test('hides div', function () { + const block = this.setUpBlockWithField(); + const field = Array.from(block.getFields())[0]; + Blockly.DropDownDiv.showPositionedByField(field); + + Blockly.DropDownDiv.hideWithoutAnimation(); + + // Technically this will trigger a CSS animation, but the property is still set to 0. + const dropDownDivElem = document.querySelector('.blocklyDropDownDiv'); + assert.strictEqual(dropDownDivElem.style.opacity, '0'); + }); + + test('hide callback calls callback', function () { + const block = this.setUpBlockWithField(); + const field = Array.from(block.getFields())[0]; + const onHideCallback = sinon.stub(); + Blockly.DropDownDiv.showPositionedByField(field, onHideCallback); + + Blockly.DropDownDiv.hideWithoutAnimation(); + + // Hiding the div should trigger the hide callback. + assert.strictEqual(onHideCallback.callCount, 1); + }); + + test('without ephemeral focus does not change focus', function () { + const block = this.setUpBlockWithField(); + const field = Array.from(block.getFields())[0]; + Blockly.getFocusManager().focusNode(block); + Blockly.DropDownDiv.showPositionedByField(field, null, null, false); + + Blockly.DropDownDiv.hideWithoutAnimation(); + + // Hiding the div shouldn't change what would have already been focused. + const blockFocusableElem = block.getFocusableElement(); + assert.strictEqual(Blockly.getFocusManager().getFocusedNode(), block); + assert.strictEqual(document.activeElement, blockFocusableElem); + }); + + test('with ephemeral focus restores DOM focus', function () { + const block = this.setUpBlockWithField(); + const field = Array.from(block.getFields())[0]; + Blockly.getFocusManager().focusNode(block); + Blockly.DropDownDiv.showPositionedByField(field, null, null, true); + + Blockly.DropDownDiv.hideWithoutAnimation(); + + // Hiding the div should restore focus back to the block. + const blockFocusableElem = block.getFocusableElement(); + assert.strictEqual(Blockly.getFocusManager().getFocusedNode(), block); + assert.strictEqual(document.activeElement, blockFocusableElem); + }); + }); + + suite('for div positioned by block', function () { + test('hides div', function () { + const block = this.setUpBlockWithField(); + const field = Array.from(block.getFields())[0]; + Blockly.DropDownDiv.showPositionedByBlock(field, block); + + Blockly.DropDownDiv.hideWithoutAnimation(); + + // Technically this will trigger a CSS animation, but the property is still set to 0. + const dropDownDivElem = document.querySelector('.blocklyDropDownDiv'); + assert.strictEqual(dropDownDivElem.style.opacity, '0'); + }); + + test('hide callback calls callback', function () { + const block = this.setUpBlockWithField(); + const field = Array.from(block.getFields())[0]; + const onHideCallback = sinon.stub(); + Blockly.DropDownDiv.showPositionedByBlock(field, block, onHideCallback); + + Blockly.DropDownDiv.hideWithoutAnimation(); + + // Hiding the div should trigger the hide callback. + assert.strictEqual(onHideCallback.callCount, 1); + }); + + test('without ephemeral focus does not change focus', function () { + const block = this.setUpBlockWithField(); + const field = Array.from(block.getFields())[0]; + Blockly.getFocusManager().focusNode(block); + Blockly.DropDownDiv.showPositionedByBlock( + field, + block, + null, + null, + false, + ); + + Blockly.DropDownDiv.hideWithoutAnimation(); + + // Hiding the div shouldn't change what would have already been focused. + const blockFocusableElem = block.getFocusableElement(); + assert.strictEqual(Blockly.getFocusManager().getFocusedNode(), block); + assert.strictEqual(document.activeElement, blockFocusableElem); + }); + + test('with ephemeral focus restores DOM focus', function () { + const block = this.setUpBlockWithField(); + const field = Array.from(block.getFields())[0]; + Blockly.getFocusManager().focusNode(block); + Blockly.DropDownDiv.showPositionedByBlock( + field, + block, + null, + null, + true, + ); + + Blockly.DropDownDiv.hideWithoutAnimation(); + + // Hiding the div should restore focus back to the block. + const blockFocusableElem = block.getFocusableElement(); + assert.strictEqual(Blockly.getFocusManager().getFocusedNode(), block); + assert.strictEqual(document.activeElement, blockFocusableElem); + }); + }); + }); +}); diff --git a/tests/mocha/event_block_change_test.js b/packages/blockly/tests/mocha/event_block_change_test.js similarity index 98% rename from tests/mocha/event_block_change_test.js rename to packages/blockly/tests/mocha/event_block_change_test.js index 7de0a23b607..9e1f9c3103e 100644 --- a/tests/mocha/event_block_change_test.js +++ b/packages/blockly/tests/mocha/event_block_change_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {defineMutatorBlocks} from './test_helpers/block_definitions.js'; import { sharedTestSetup, diff --git a/tests/mocha/event_block_create_test.js b/packages/blockly/tests/mocha/event_block_create_test.js similarity index 98% rename from tests/mocha/event_block_create_test.js rename to packages/blockly/tests/mocha/event_block_create_test.js index f59f9435efd..1672b56bb98 100644 --- a/tests/mocha/event_block_create_test.js +++ b/packages/blockly/tests/mocha/event_block_create_test.js @@ -5,7 +5,7 @@ */ import {EventType} from '../../build/src/core/events/type.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {defineRowBlock} from './test_helpers/block_definitions.js'; import {assertEventFired} from './test_helpers/events.js'; import { diff --git a/tests/mocha/event_block_delete_test.js b/packages/blockly/tests/mocha/event_block_delete_test.js similarity index 96% rename from tests/mocha/event_block_delete_test.js rename to packages/blockly/tests/mocha/event_block_delete_test.js index d74b6aa062b..e2fb5b8ce88 100644 --- a/tests/mocha/event_block_delete_test.js +++ b/packages/blockly/tests/mocha/event_block_delete_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {defineRowBlock} from './test_helpers/block_definitions.js'; import { sharedTestSetup, diff --git a/tests/mocha/event_block_drag_test.js b/packages/blockly/tests/mocha/event_block_drag_test.js similarity index 94% rename from tests/mocha/event_block_drag_test.js rename to packages/blockly/tests/mocha/event_block_drag_test.js index 9b0f2031ad0..cc71e3bf084 100644 --- a/tests/mocha/event_block_drag_test.js +++ b/packages/blockly/tests/mocha/event_block_drag_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {defineRowBlock} from './test_helpers/block_definitions.js'; import { sharedTestSetup, diff --git a/tests/mocha/event_block_field_intermediate_change_test.js b/packages/blockly/tests/mocha/event_block_field_intermediate_change_test.js similarity index 96% rename from tests/mocha/event_block_field_intermediate_change_test.js rename to packages/blockly/tests/mocha/event_block_field_intermediate_change_test.js index 0ff4e1bbf3c..d917dadcdd5 100644 --- a/tests/mocha/event_block_field_intermediate_change_test.js +++ b/packages/blockly/tests/mocha/event_block_field_intermediate_change_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/event_block_move_test.js b/packages/blockly/tests/mocha/event_block_move_test.js similarity index 94% rename from tests/mocha/event_block_move_test.js rename to packages/blockly/tests/mocha/event_block_move_test.js index b93457e14c1..6d1890eebeb 100644 --- a/tests/mocha/event_block_move_test.js +++ b/packages/blockly/tests/mocha/event_block_move_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {defineRowBlock} from './test_helpers/block_definitions.js'; import { sharedTestSetup, diff --git a/tests/mocha/event_bubble_open_test.js b/packages/blockly/tests/mocha/event_bubble_open_test.js similarity index 94% rename from tests/mocha/event_bubble_open_test.js rename to packages/blockly/tests/mocha/event_bubble_open_test.js index 099a625f6e2..a445a6a7819 100644 --- a/tests/mocha/event_bubble_open_test.js +++ b/packages/blockly/tests/mocha/event_bubble_open_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {defineMutatorBlocks} from './test_helpers/block_definitions.js'; import { sharedTestSetup, diff --git a/tests/mocha/event_click_test.js b/packages/blockly/tests/mocha/event_click_test.js similarity index 94% rename from tests/mocha/event_click_test.js rename to packages/blockly/tests/mocha/event_click_test.js index 6e18769485b..5c4afbcadf9 100644 --- a/tests/mocha/event_click_test.js +++ b/packages/blockly/tests/mocha/event_click_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {defineRowBlock} from './test_helpers/block_definitions.js'; import { sharedTestSetup, diff --git a/tests/mocha/event_comment_change_test.js b/packages/blockly/tests/mocha/event_comment_change_test.js similarity index 94% rename from tests/mocha/event_comment_change_test.js rename to packages/blockly/tests/mocha/event_comment_change_test.js index ed5f4d9f6ae..edb539ef555 100644 --- a/tests/mocha/event_comment_change_test.js +++ b/packages/blockly/tests/mocha/event_comment_change_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/event_comment_collapse_test.js b/packages/blockly/tests/mocha/event_comment_collapse_test.js similarity index 93% rename from tests/mocha/event_comment_collapse_test.js rename to packages/blockly/tests/mocha/event_comment_collapse_test.js index e2d27530708..5c3f61054a1 100644 --- a/tests/mocha/event_comment_collapse_test.js +++ b/packages/blockly/tests/mocha/event_comment_collapse_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/event_comment_create_test.js b/packages/blockly/tests/mocha/event_comment_create_test.js similarity index 94% rename from tests/mocha/event_comment_create_test.js rename to packages/blockly/tests/mocha/event_comment_create_test.js index df919541d95..71ef8ed1b75 100644 --- a/tests/mocha/event_comment_create_test.js +++ b/packages/blockly/tests/mocha/event_comment_create_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/event_comment_delete_test.js b/packages/blockly/tests/mocha/event_comment_delete_test.js similarity index 94% rename from tests/mocha/event_comment_delete_test.js rename to packages/blockly/tests/mocha/event_comment_delete_test.js index 2e2bb45c491..dd9f0dd2286 100644 --- a/tests/mocha/event_comment_delete_test.js +++ b/packages/blockly/tests/mocha/event_comment_delete_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/event_comment_drag_test.js b/packages/blockly/tests/mocha/event_comment_drag_test.js similarity index 93% rename from tests/mocha/event_comment_drag_test.js rename to packages/blockly/tests/mocha/event_comment_drag_test.js index d214e0adba1..f6685cc5bdf 100644 --- a/tests/mocha/event_comment_drag_test.js +++ b/packages/blockly/tests/mocha/event_comment_drag_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/event_comment_move_test.js b/packages/blockly/tests/mocha/event_comment_move_test.js similarity index 95% rename from tests/mocha/event_comment_move_test.js rename to packages/blockly/tests/mocha/event_comment_move_test.js index aae3fdfe632..b3acea990a6 100644 --- a/tests/mocha/event_comment_move_test.js +++ b/packages/blockly/tests/mocha/event_comment_move_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/event_comment_resize_test.js b/packages/blockly/tests/mocha/event_comment_resize_test.js similarity index 94% rename from tests/mocha/event_comment_resize_test.js rename to packages/blockly/tests/mocha/event_comment_resize_test.js index b74e1abb2bf..bed3e733a65 100644 --- a/tests/mocha/event_comment_resize_test.js +++ b/packages/blockly/tests/mocha/event_comment_resize_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/event_selected_test.js b/packages/blockly/tests/mocha/event_selected_test.js similarity index 94% rename from tests/mocha/event_selected_test.js rename to packages/blockly/tests/mocha/event_selected_test.js index 1ce8306db48..8731099ec96 100644 --- a/tests/mocha/event_selected_test.js +++ b/packages/blockly/tests/mocha/event_selected_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {defineRowBlock} from './test_helpers/block_definitions.js'; import { sharedTestSetup, diff --git a/tests/mocha/event_test.js b/packages/blockly/tests/mocha/event_test.js similarity index 91% rename from tests/mocha/event_test.js rename to packages/blockly/tests/mocha/event_test.js index 84ea7f0d78e..24bbf6bf505 100644 --- a/tests/mocha/event_test.js +++ b/packages/blockly/tests/mocha/event_test.js @@ -6,8 +6,7 @@ import * as Blockly from '../../build/src/core/blockly.js'; import * as eventUtils from '../../build/src/core/events/utils.js'; -import {ASTNode} from '../../build/src/core/keyboard_nav/ast_node.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { assertEventEquals, assertNthCallEventArgEquals, @@ -356,6 +355,7 @@ suite('Events', function () { suite('With variable getter blocks', function () { setup(function () { + this.TEST_BLOCK_ID = 'test_block_id'; this.genUidStub = createGenUidStubWithReturns([ this.TEST_BLOCK_ID, 'test_var_id', @@ -519,85 +519,6 @@ suite('Events', function () { newValue: 'new value', }), }, - { - title: 'null to Block Marker move', - class: Blockly.Events.MarkerMove, - getArgs: (thisObj) => [ - thisObj.block, - true, - null, - new ASTNode(ASTNode.types.BLOCK, thisObj.block), - ], - getExpectedJson: (thisObj) => ({ - type: 'marker_move', - group: '', - isCursor: true, - blockId: thisObj.block.id, - oldNode: undefined, - newNode: new ASTNode(ASTNode.types.BLOCK, thisObj.block), - }), - }, - { - title: 'null to Workspace Marker move', - class: Blockly.Events.MarkerMove, - getArgs: (thisObj) => [ - null, - true, - null, - ASTNode.createWorkspaceNode( - thisObj.workspace, - new Blockly.utils.Coordinate(0, 0), - ), - ], - getExpectedJson: (thisObj) => ({ - type: 'marker_move', - group: '', - isCursor: true, - blockId: undefined, - oldNode: undefined, - newNode: ASTNode.createWorkspaceNode( - thisObj.workspace, - new Blockly.utils.Coordinate(0, 0), - ), - }), - }, - { - title: 'Workspace to Block Marker move', - class: Blockly.Events.MarkerMove, - getArgs: (thisObj) => [ - thisObj.block, - true, - ASTNode.createWorkspaceNode( - thisObj.workspace, - new Blockly.utils.Coordinate(0, 0), - ), - new ASTNode(ASTNode.types.BLOCK, thisObj.block), - ], - getExpectedJson: (thisObj) => ({ - type: 'marker_move', - group: '', - isCursor: true, - blockId: thisObj.block.id, - oldNode: ASTNode.createWorkspaceNode( - thisObj.workspace, - new Blockly.utils.Coordinate(0, 0), - ), - newNode: new ASTNode(ASTNode.types.BLOCK, thisObj.block), - }), - }, - { - title: 'Block to Workspace Marker move', - class: Blockly.Events.MarkerMove, - getArgs: (thisObj) => [ - null, - true, - new ASTNode(ASTNode.types.BLOCK, thisObj.block), - ASTNode.createWorkspaceNode( - thisObj.workspace, - new Blockly.utils.Coordinate(0, 0), - ), - ], - }, { title: 'Selected', class: Blockly.Events.Selected, @@ -912,11 +833,9 @@ suite('Events', function () { title: 'Variable events', testCases: variableEventTestCases, setup: (thisObj) => { - thisObj.variable = thisObj.workspace.createVariable( - 'name1', - 'type1', - 'id1', - ); + thisObj.variable = thisObj.workspace + .getVariableMap() + .createVariable('name1', 'type1', 'id1'); }, }, { @@ -983,19 +902,20 @@ suite('Events', function () { suite('Variable events', function () { setup(function () { - this.variable = this.workspace.createVariable('name1', 'type1', 'id1'); + this.variableMap = this.workspace.getVariableMap(); + this.variable = this.variableMap.createVariable('name1', 'type1', 'id1'); }); /** * Check if a variable with the given values exists. - * @param {Blockly.Workspace|Blockly.VariableMap} container The workspace or - * variableMap the checked variable belongs to. + * @param {Blockly.VariableMap} variableMap The variableMap + * the checked variable belongs to. * @param {!string} name The expected name of the variable. * @param {!string} type The expected type of the variable. * @param {!string} id The expected id of the variable. */ - function checkVariableValues(container, name, type, id) { - const variable = container.getVariableById(id); + function checkVariableValues(variableMap, name, type, id) { + const variable = variableMap.getVariableById(id); assert.isDefined(variable); assert.equal(name, variable.name); assert.equal(type, variable.type); @@ -1073,7 +993,7 @@ suite('Events', function () { varName: 'name2', }; const event = eventUtils.fromJson(json, this.workspace); - const x = this.workspace.getVariableById('id2'); + const x = this.variableMap.getVariableById('id2'); assert.isNull(x); event.run(true); assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); @@ -1081,22 +1001,22 @@ suite('Events', function () { test('Var delete', function () { const event = new Blockly.Events.VarDelete(this.variable); - assert.isNotNull(this.workspace.getVariableById('id1')); + assert.isNotNull(this.variableMap.getVariableById('id1')); event.run(true); - assert.isNull(this.workspace.getVariableById('id1')); + assert.isNull(this.variableMap.getVariableById('id1')); }); test('Var rename', function () { const event = new Blockly.Events.VarRename(this.variable, 'name2'); event.run(true); - assert.isNull(this.workspace.getVariable('name1')); - checkVariableValues(this.workspace, 'name2', 'type1', 'id1'); + assert.isNull(this.variableMap.getVariable('name1')); + checkVariableValues(this.variableMap, 'name2', 'type1', 'id1'); }); }); suite('Run Backward', function () { test('Var create', function () { const event = new Blockly.Events.VarCreate(this.variable); - assert.isNotNull(this.workspace.getVariableById('id1')); + assert.isNotNull(this.variableMap.getVariableById('id1')); event.run(false); }); @@ -1108,16 +1028,16 @@ suite('Events', function () { varName: 'name2', }; const event = eventUtils.fromJson(json, this.workspace); - assert.isNull(this.workspace.getVariableById('id2')); + assert.isNull(this.variableMap.getVariableById('id2')); event.run(false); - assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); + assertVariableValues(this.variableMap, 'name2', 'type2', 'id2'); }); test('Var rename', function () { const event = new Blockly.Events.VarRename(this.variable, 'name2'); event.run(false); - assert.isNull(this.workspace.getVariable('name2')); - checkVariableValues(this.workspace, 'name1', 'type1', 'id1'); + assert.isNull(this.variableMap.getVariable('name2')); + checkVariableValues(this.variableMap, 'name1', 'type1', 'id1'); }); }); }); @@ -1302,7 +1222,7 @@ suite('Events', function () { new Blockly.Events.BlockChange(block, 'field', 'VAR', 'id1', 'id2'), new Blockly.Events.Click(block), ]; - const filteredEvents = eventUtils.filter(events, true); + const filteredEvents = eventUtils.filter(events); assert.equal(filteredEvents.length, 4); // no event should have been removed. // test that the order hasn't changed assert.isTrue(filteredEvents[0] instanceof Blockly.Events.BlockCreate); @@ -1320,7 +1240,7 @@ suite('Events', function () { new Blockly.Events.BlockCreate(block2), new Blockly.Events.BlockMove(block2), ]; - const filteredEvents = eventUtils.filter(events, true); + const filteredEvents = eventUtils.filter(events); assert.equal(filteredEvents.length, 4); // no event should have been removed. }); @@ -1330,7 +1250,7 @@ suite('Events', function () { addMoveEvent(events, block, 1, 1); addMoveEvent(events, block, 2, 2); addMoveEvent(events, block, 3, 3); - const filteredEvents = eventUtils.filter(events, true); + const filteredEvents = eventUtils.filter(events); assert.equal(filteredEvents.length, 2); // duplicate moves should have been removed. // test that the order hasn't changed assert.isTrue(filteredEvents[0] instanceof Blockly.Events.BlockCreate); @@ -1339,27 +1259,12 @@ suite('Events', function () { assert.equal(filteredEvents[1].newCoordinate.y, 3); }); - test('Backward', function () { - const block = this.workspace.newBlock('field_variable_test_block', '1'); - const events = [new Blockly.Events.BlockCreate(block)]; - addMoveEvent(events, block, 1, 1); - addMoveEvent(events, block, 2, 2); - addMoveEvent(events, block, 3, 3); - const filteredEvents = eventUtils.filter(events, false); - assert.equal(filteredEvents.length, 2); // duplicate event should have been removed. - // test that the order hasn't changed - assert.isTrue(filteredEvents[0] instanceof Blockly.Events.BlockCreate); - assert.isTrue(filteredEvents[1] instanceof Blockly.Events.BlockMove); - assert.equal(filteredEvents[1].newCoordinate.x, 1); - assert.equal(filteredEvents[1].newCoordinate.y, 1); - }); - test('Merge block move events', function () { const block = this.workspace.newBlock('field_variable_test_block', '1'); const events = []; addMoveEvent(events, block, 0, 0); addMoveEvent(events, block, 1, 1); - const filteredEvents = eventUtils.filter(events, true); + const filteredEvents = eventUtils.filter(events); assert.equal(filteredEvents.length, 1); // second move event merged into first assert.equal(filteredEvents[0].newCoordinate.x, 1); assert.equal(filteredEvents[0].newCoordinate.y, 1); @@ -1377,7 +1282,7 @@ suite('Events', function () { 'item2', ), ]; - const filteredEvents = eventUtils.filter(events, true); + const filteredEvents = eventUtils.filter(events); assert.equal(filteredEvents.length, 1); // second change event merged into first assert.equal(filteredEvents[0].oldValue, 'item'); assert.equal(filteredEvents[0].newValue, 'item2'); @@ -1388,7 +1293,7 @@ suite('Events', function () { new Blockly.Events.ViewportChange(1, 2, 3, this.workspace, 4), new Blockly.Events.ViewportChange(5, 6, 7, this.workspace, 8), ]; - const filteredEvents = eventUtils.filter(events, true); + const filteredEvents = eventUtils.filter(events); assert.equal(filteredEvents.length, 1); // second change event merged into first assert.equal(filteredEvents[0].viewTop, 5); assert.equal(filteredEvents[0].viewLeft, 6); @@ -1408,7 +1313,7 @@ suite('Events', function () { new Blockly.Events.BubbleOpen(block3, true, 'warning'), new Blockly.Events.Click(block3), ]; - const filteredEvents = eventUtils.filter(events, true); + const filteredEvents = eventUtils.filter(events); // click event merged into corresponding *Open event assert.equal(filteredEvents.length, 3); assert.isTrue(filteredEvents[0] instanceof Blockly.Events.BubbleOpen); @@ -1427,7 +1332,7 @@ suite('Events', function () { new Blockly.Events.Click(block), new Blockly.Events.BlockDrag(block, true), ]; - const filteredEvents = eventUtils.filter(events, true); + const filteredEvents = eventUtils.filter(events); // click and stackclick should both exist assert.equal(filteredEvents.length, 2); assert.isTrue(filteredEvents[0] instanceof Blockly.Events.Click); @@ -1447,7 +1352,7 @@ suite('Events', function () { const events = []; addMoveEventParent(events, block, null); addMoveEventParent(events, block, null); - const filteredEvents = eventUtils.filter(events, true); + const filteredEvents = eventUtils.filter(events); // The two events should be merged, but because nothing has changed // they will be filtered out. assert.equal(filteredEvents.length, 0); @@ -1468,7 +1373,7 @@ suite('Events', function () { events.push(new Blockly.Events.BlockDelete(block2)); addMoveEvent(events, block1, 2, 2); - const filteredEvents = eventUtils.filter(events, true); + const filteredEvents = eventUtils.filter(events); // Nothing should have merged. assert.equal(filteredEvents.length, 4); // test that the order hasn't changed @@ -1525,7 +1430,9 @@ suite('Events', function () { ); // Expect the workspace to not have a variable with ID 'test_block_id'. - assert.isNull(this.workspace.getVariableById(TEST_BLOCK_ID)); + assert.isNull( + this.workspace.getVariableMap().getVariableById(TEST_BLOCK_ID), + ); } finally { workspaceTeardown.call(this, workspaceSvg); } @@ -1575,7 +1482,9 @@ suite('Events', function () { ); // Expect the workspace to have a variable with ID 'test_var_id'. - assert.isNotNull(this.workspace.getVariableById(TEST_VAR_ID)); + assert.isNotNull( + this.workspace.getVariableMap().getVariableById(TEST_VAR_ID), + ); }); test('New block new var xml', function () { @@ -1639,7 +1548,9 @@ suite('Events', function () { ); // Expect the workspace to have a variable with ID 'test_var_id'. - assert.isNotNull(this.workspace.getVariableById(TEST_VAR_ID)); + assert.isNotNull( + this.workspace.getVariableMap().getVariableById(TEST_VAR_ID), + ); }); }); suite('Disable orphans', function () { diff --git a/tests/mocha/event_theme_change_test.js b/packages/blockly/tests/mocha/event_theme_change_test.js similarity index 93% rename from tests/mocha/event_theme_change_test.js rename to packages/blockly/tests/mocha/event_theme_change_test.js index f20f745b6a0..396347c9e13 100644 --- a/tests/mocha/event_theme_change_test.js +++ b/packages/blockly/tests/mocha/event_theme_change_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/event_toolbox_item_select_test.js b/packages/blockly/tests/mocha/event_toolbox_item_select_test.js similarity index 96% rename from tests/mocha/event_toolbox_item_select_test.js rename to packages/blockly/tests/mocha/event_toolbox_item_select_test.js index bf6a9a46212..02484c35bc1 100644 --- a/tests/mocha/event_toolbox_item_select_test.js +++ b/packages/blockly/tests/mocha/event_toolbox_item_select_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/event_trashcan_open_test.js b/packages/blockly/tests/mocha/event_trashcan_open_test.js similarity index 93% rename from tests/mocha/event_trashcan_open_test.js rename to packages/blockly/tests/mocha/event_trashcan_open_test.js index 2c809f2dfad..47da09a075a 100644 --- a/tests/mocha/event_trashcan_open_test.js +++ b/packages/blockly/tests/mocha/event_trashcan_open_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/event_var_create_test.js b/packages/blockly/tests/mocha/event_var_create_test.js similarity index 95% rename from tests/mocha/event_var_create_test.js rename to packages/blockly/tests/mocha/event_var_create_test.js index e374c496541..79af41281de 100644 --- a/tests/mocha/event_var_create_test.js +++ b/packages/blockly/tests/mocha/event_var_create_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/event_var_delete_test.js b/packages/blockly/tests/mocha/event_var_delete_test.js similarity index 95% rename from tests/mocha/event_var_delete_test.js rename to packages/blockly/tests/mocha/event_var_delete_test.js index b06943d9a19..93d9ef0ba2d 100644 --- a/tests/mocha/event_var_delete_test.js +++ b/packages/blockly/tests/mocha/event_var_delete_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/event_var_rename_test.js b/packages/blockly/tests/mocha/event_var_rename_test.js similarity index 93% rename from tests/mocha/event_var_rename_test.js rename to packages/blockly/tests/mocha/event_var_rename_test.js index 7fbd185ab7b..b6d77cb35bd 100644 --- a/tests/mocha/event_var_rename_test.js +++ b/packages/blockly/tests/mocha/event_var_rename_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/packages/blockly/tests/mocha/event_var_type_change_test.js b/packages/blockly/tests/mocha/event_var_type_change_test.js new file mode 100644 index 00000000000..066c145a3ef --- /dev/null +++ b/packages/blockly/tests/mocha/event_var_type_change_test.js @@ -0,0 +1,43 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {assert} from '../../node_modules/chai/index.js'; +import { + sharedTestSetup, + sharedTestTeardown, +} from './test_helpers/setup_teardown.js'; + +suite('Var Type Change Event', function () { + setup(function () { + sharedTestSetup.call(this); + this.workspace = new Blockly.Workspace(); + }); + + teardown(function () { + sharedTestTeardown.call(this); + }); + + suite('Serialization', function () { + test('variable type change events round-trip through JSON', function () { + const varModel = new Blockly.VariableModel( + this.workspace, + 'name', + 'foo', + 'id', + ); + const origEvent = new Blockly.Events.VarTypeChange( + varModel, + 'foo', + 'bar', + ); + + const json = origEvent.toJson(); + const newEvent = new Blockly.Events.fromJson(json, this.workspace); + + assert.deepEqual(newEvent, origEvent); + }); + }); +}); diff --git a/tests/mocha/event_viewport_test.js b/packages/blockly/tests/mocha/event_viewport_test.js similarity index 93% rename from tests/mocha/event_viewport_test.js rename to packages/blockly/tests/mocha/event_viewport_test.js index edacc0da6cb..cd11079fa32 100644 --- a/tests/mocha/event_viewport_test.js +++ b/packages/blockly/tests/mocha/event_viewport_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/extensions_test.js b/packages/blockly/tests/mocha/extensions_test.js similarity index 99% rename from tests/mocha/extensions_test.js rename to packages/blockly/tests/mocha/extensions_test.js index 66772cbea4b..8c41861d5d8 100644 --- a/tests/mocha/extensions_test.js +++ b/packages/blockly/tests/mocha/extensions_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/field_checkbox_test.js b/packages/blockly/tests/mocha/field_checkbox_test.js similarity index 99% rename from tests/mocha/field_checkbox_test.js rename to packages/blockly/tests/mocha/field_checkbox_test.js index 08190fed823..74357338a5a 100644 --- a/tests/mocha/field_checkbox_test.js +++ b/packages/blockly/tests/mocha/field_checkbox_test.js @@ -5,7 +5,7 @@ */ import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {defineRowBlock} from './test_helpers/block_definitions.js'; import { assertFieldValue, diff --git a/tests/mocha/field_colour_test.js b/packages/blockly/tests/mocha/field_colour_test.js similarity index 99% rename from tests/mocha/field_colour_test.js rename to packages/blockly/tests/mocha/field_colour_test.js index 262f978f29d..975d5a01d4a 100644 --- a/tests/mocha/field_colour_test.js +++ b/packages/blockly/tests/mocha/field_colour_test.js @@ -5,7 +5,7 @@ */ import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { createTestBlock, defineRowBlock, diff --git a/tests/mocha/field_dropdown_test.js b/packages/blockly/tests/mocha/field_dropdown_test.js similarity index 80% rename from tests/mocha/field_dropdown_test.js rename to packages/blockly/tests/mocha/field_dropdown_test.js index 61deaf47f39..a1731e81281 100644 --- a/tests/mocha/field_dropdown_test.js +++ b/packages/blockly/tests/mocha/field_dropdown_test.js @@ -5,7 +5,7 @@ */ import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { createTestBlock, defineRowBlock, @@ -92,9 +92,9 @@ suite('Dropdown Fields', function () { expectedText: 'a', args: [ [ - [{src: 'scrA', alt: 'a'}, 'A'], - [{src: 'scrB', alt: 'b'}, 'B'], - [{src: 'scrC', alt: 'c'}, 'C'], + [{src: 'scrA', alt: 'a', width: 10, height: 10}, 'A'], + [{src: 'scrB', alt: 'b', width: 10, height: 10}, 'B'], + [{src: 'scrC', alt: 'c', width: 10, height: 10}, 'C'], ], ], }, @@ -121,9 +121,9 @@ suite('Dropdown Fields', function () { args: [ () => { return [ - [{src: 'scrA', alt: 'a'}, 'A'], - [{src: 'scrB', alt: 'b'}, 'B'], - [{src: 'scrC', alt: 'c'}, 'C'], + [{src: 'scrA', alt: 'a', width: 10, height: 10}, 'A'], + [{src: 'scrB', alt: 'b', width: 10, height: 10}, 'B'], + [{src: 'scrC', alt: 'c', width: 10, height: 10}, 'C'], ]; }, ], @@ -195,6 +195,52 @@ suite('Dropdown Fields', function () { assertFieldValue(this.field, 'B', 'b'); }); }); + suite('setOptions', function () { + setup(function () { + this.field = new Blockly.FieldDropdown([ + ['a', 'A'], + ['b', 'B'], + ['c', 'C'], + ]); + }); + test('With array updates options', function () { + this.field.setOptions([ + ['d', 'D'], + ['e', 'E'], + ['f', 'F'], + ]); + assertFieldValue(this.field, 'D', 'd'); + }); + test('With generator updates options', function () { + this.field.setOptions(function () { + return [ + ['d', 'D'], + ['e', 'E'], + ['f', 'F'], + ]; + }); + assertFieldValue(this.field, 'D', 'd'); + }); + test('With trimmable options gets trimmed', function () { + this.field.setOptions([ + ['a d b', 'D'], + ['a e b', 'E'], + ['a f b', 'F'], + ]); + assert.deepEqual(this.field.prefixField, 'a'); + assert.deepEqual(this.field.suffixField, 'b'); + assert.deepEqual(this.field.getOptions(), [ + ['d', 'D'], + ['e', 'E'], + ['f', 'F'], + ]); + }); + test('With an empty array of options throws', function () { + assert.throws(function () { + this.field.setOptions([]); + }); + }); + }); suite('Validators', function () { setup(function () { diff --git a/tests/mocha/field_image_test.js b/packages/blockly/tests/mocha/field_image_test.js similarity index 63% rename from tests/mocha/field_image_test.js rename to packages/blockly/tests/mocha/field_image_test.js index 89dd5fcc91b..f0358703bdf 100644 --- a/tests/mocha/field_image_test.js +++ b/packages/blockly/tests/mocha/field_image_test.js @@ -5,7 +5,7 @@ */ import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { assertFieldValue, runConstructorSuiteTests, @@ -20,6 +20,7 @@ import { suite('Image Fields', function () { setup(function () { sharedTestSetup.call(this); + this.workspace = Blockly.inject('blocklyDiv'); }); teardown(function () { sharedTestTeardown.call(this); @@ -237,5 +238,114 @@ suite('Image Fields', function () { assert.isTrue(field.getFlipRtl()); }); }); + suite('isClickable', function () { + setup(function () { + this.onClick = function () { + console.log('on click'); + }; + this.setUpBlockWithFieldImages = function () { + const blockJson = { + 'type': 'text', + 'id': 'block_id', + 'x': 0, + 'y': 0, + 'fields': { + 'TEXT': '', + }, + }; + Blockly.serialization.blocks.append(blockJson, this.workspace); + return this.workspace.getBlockById('block_id'); + }; + this.extractFieldImage = function (block) { + const fields = Array.from(block.getFields()); + // Sanity check (as a precondition). + assert.strictEqual(fields.length, 3); + const imageField = fields[0]; + // Sanity check (as a precondition). + assert.isTrue(imageField instanceof Blockly.FieldImage); + return imageField; + }; + }); + + test('Unattached field without click handler returns false', function () { + const field = new Blockly.FieldImage('src', 10, 10, null); + + const isClickable = field.isClickable(); + + assert.isFalse(isClickable); + }); + test('Unattached field with click handler returns false', function () { + const field = new Blockly.FieldImage('src', 10, 10, this.onClick); + + const isClickable = field.isClickable(); + + assert.isFalse(isClickable); + }); + test('For attached but disabled field without click handler returns false', function () { + const block = this.setUpBlockWithFieldImages(); + const field = this.extractFieldImage(block); + field.setEnabled(false); + + const isClickable = field.isClickable(); + + assert.isFalse(isClickable); + }); + test('For attached but disabled field with click handler returns false', function () { + const block = this.setUpBlockWithFieldImages(); + const field = this.extractFieldImage(block); + field.setEnabled(false); + field.setOnClickHandler(this.onClick); + + const isClickable = field.isClickable(); + + assert.isFalse(isClickable); + }); + test('For attached, enabled, but not editable field without click handler returns false', function () { + const block = this.setUpBlockWithFieldImages(); + const field = this.extractFieldImage(block); + block.setEditable(false); + + const isClickable = field.isClickable(); + + assert.isFalse(isClickable); + }); + test('For attached, enabled, but not editable field with click handler returns false', function () { + const block = this.setUpBlockWithFieldImages(); + const field = this.extractFieldImage(block); + block.setEditable(false); + field.setOnClickHandler(this.onClick); + + const isClickable = field.isClickable(); + + assert.isFalse(isClickable); + }); + test('For attached, enabled, editable field without click handler returns false', function () { + const block = this.setUpBlockWithFieldImages(); + const field = this.extractFieldImage(block); + + const isClickable = field.isClickable(); + + assert.isFalse(isClickable); + }); + test('For attached, enabled, editable field with click handler returns true', function () { + const block = this.setUpBlockWithFieldImages(); + const field = this.extractFieldImage(block); + field.setOnClickHandler(this.onClick); + + const isClickable = field.isClickable(); + + assert.isTrue(isClickable); + }); + test('For attached, enabled, editable field with removed click handler returns false', function () { + const block = this.setUpBlockWithFieldImages(); + const field = this.extractFieldImage(block); + field.setOnClickHandler(this.onClick); + field.setOnClickHandler(null); + + const isClickable = field.isClickable(); + + assert.isFalse(isClickable); + }); + }); }); }); diff --git a/tests/mocha/field_label_serializable_test.js b/packages/blockly/tests/mocha/field_label_serializable_test.js similarity index 99% rename from tests/mocha/field_label_serializable_test.js rename to packages/blockly/tests/mocha/field_label_serializable_test.js index a831713412c..443cc6d1753 100644 --- a/tests/mocha/field_label_serializable_test.js +++ b/packages/blockly/tests/mocha/field_label_serializable_test.js @@ -5,7 +5,7 @@ */ import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { createTestBlock, defineRowBlock, diff --git a/tests/mocha/field_label_test.js b/packages/blockly/tests/mocha/field_label_test.js similarity index 99% rename from tests/mocha/field_label_test.js rename to packages/blockly/tests/mocha/field_label_test.js index cf5b4904493..bae600aff19 100644 --- a/tests/mocha/field_label_test.js +++ b/packages/blockly/tests/mocha/field_label_test.js @@ -5,7 +5,7 @@ */ import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {createTestBlock} from './test_helpers/block_definitions.js'; import { assertFieldValue, diff --git a/tests/mocha/field_number_test.js b/packages/blockly/tests/mocha/field_number_test.js similarity index 99% rename from tests/mocha/field_number_test.js rename to packages/blockly/tests/mocha/field_number_test.js index 768766bf013..3c12fed820d 100644 --- a/tests/mocha/field_number_test.js +++ b/packages/blockly/tests/mocha/field_number_test.js @@ -5,7 +5,7 @@ */ import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {defineRowBlock} from './test_helpers/block_definitions.js'; import {runTestCases} from './test_helpers/common.js'; import { diff --git a/tests/mocha/field_registry_test.js b/packages/blockly/tests/mocha/field_registry_test.js similarity index 98% rename from tests/mocha/field_registry_test.js rename to packages/blockly/tests/mocha/field_registry_test.js index 26b33c16c3d..1f19477dee1 100644 --- a/tests/mocha/field_registry_test.js +++ b/packages/blockly/tests/mocha/field_registry_test.js @@ -5,7 +5,7 @@ */ import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/field_test.js b/packages/blockly/tests/mocha/field_test.js similarity index 99% rename from tests/mocha/field_test.js rename to packages/blockly/tests/mocha/field_test.js index 38f9662d6d6..422b0473418 100644 --- a/tests/mocha/field_test.js +++ b/packages/blockly/tests/mocha/field_test.js @@ -5,7 +5,7 @@ */ import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { addBlockTypeToCleanup, addMessageToCleanup, diff --git a/packages/blockly/tests/mocha/field_textinput_test.js b/packages/blockly/tests/mocha/field_textinput_test.js new file mode 100644 index 00000000000..7cafd00d948 --- /dev/null +++ b/packages/blockly/tests/mocha/field_textinput_test.js @@ -0,0 +1,593 @@ +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as Blockly from '../../build/src/core/blockly.js'; +import {assert} from '../../node_modules/chai/index.js'; +import { + createTestBlock, + defineRowBlock, +} from './test_helpers/block_definitions.js'; +import { + assertFieldValue, + runConstructorSuiteTests, + runFromJsonSuiteTests, + runSetValueTests, +} from './test_helpers/fields.js'; +import { + sharedTestSetup, + sharedTestTeardown, + workspaceTeardown, +} from './test_helpers/setup_teardown.js'; + +suite('Text Input Fields', function () { + setup(function () { + sharedTestSetup.call(this); + }); + teardown(function () { + sharedTestTeardown.call(this); + }); + /** + * Configuration for field tests with invalid values. + * @type {!Array} + */ + const invalidValueTestCases = [ + {title: 'Undefined', value: undefined}, + {title: 'Null', value: null}, + ]; + /** + * Configuration for field tests with valid values. + * @type {!Array} + */ + const validValueTestCases = [ + {title: 'String', value: 'value', expectedValue: 'value'}, + {title: 'Boolean true', value: true, expectedValue: 'true'}, + {title: 'Boolean false', value: false, expectedValue: 'false'}, + {title: 'Number (Truthy)', value: 1, expectedValue: '1'}, + {title: 'Number (Falsy)', value: 0, expectedValue: '0'}, + {title: 'NaN', value: NaN, expectedValue: 'NaN'}, + ]; + const addArgsAndJson = function (testCase) { + testCase.args = [testCase.value]; + testCase.json = {'text': testCase.value}; + }; + invalidValueTestCases.forEach(addArgsAndJson); + validValueTestCases.forEach(addArgsAndJson); + + /** + * The expected default value for the field being tested. + * @type {*} + */ + const defaultFieldValue = ''; + /** + * Asserts that the field property values are set to default. + * @param {!Blockly.FieldTextInput} field The field to check. + */ + const assertFieldDefault = function (field) { + assertFieldValue(field, defaultFieldValue); + }; + /** + * Asserts that the field properties are correct based on the test case. + * @param {!Blockly.FieldTextInput} field The field to check. + * @param {!FieldValueTestCase} testCase The test case. + */ + const validTestCaseAssertField = function (field, testCase) { + assertFieldValue(field, testCase.expectedValue); + }; + + runConstructorSuiteTests( + Blockly.FieldTextInput, + validValueTestCases, + invalidValueTestCases, + validTestCaseAssertField, + assertFieldDefault, + ); + + runFromJsonSuiteTests( + Blockly.FieldTextInput, + validValueTestCases, + invalidValueTestCases, + validTestCaseAssertField, + assertFieldDefault, + ); + + suite('setValue', function () { + suite('Empty -> New Value', function () { + setup(function () { + this.field = new Blockly.FieldTextInput(); + }); + runSetValueTests( + validValueTestCases, + invalidValueTestCases, + defaultFieldValue, + ); + test('With source block', function () { + this.field.setSourceBlock(createTestBlock()); + this.field.setValue('value'); + assertFieldValue(this.field, 'value'); + }); + }); + suite('Value -> New Value', function () { + const initialValue = 'oldValue'; + setup(function () { + this.field = new Blockly.FieldTextInput(initialValue); + }); + runSetValueTests( + validValueTestCases, + invalidValueTestCases, + initialValue, + ); + test('With source block', function () { + this.field.setSourceBlock(createTestBlock()); + this.field.setValue('value'); + assertFieldValue(this.field, 'value'); + }); + }); + }); + + suite('Validators', function () { + setup(function () { + this.field = new Blockly.FieldTextInput('value'); + this.field.valueWhenEditorWasOpened_ = this.field.getValue(); + this.field.htmlInput_ = document.createElement('input'); + this.field.htmlInput_.setAttribute('data-old-value', 'value'); + this.field.htmlInput_.setAttribute('data-untyped-default-value', 'value'); + this.stub = sinon.stub(this.field, 'resizeEditor_'); + }); + teardown(function () { + sinon.restore(); + }); + const testSuites = [ + { + title: 'Null Validator', + validator: function () { + return null; + }, + value: 'newValue', + expectedValue: 'value', + }, + { + title: "Remove 'a' Validator", + validator: function (newValue) { + return newValue.replace(/a/g, ''); + }, + value: 'bbbaaa', + expectedValue: 'bbb', + }, + { + title: 'Returns Undefined Validator', + validator: function () {}, + value: 'newValue', + expectedValue: 'newValue', + expectedText: 'newValue', + }, + ]; + testSuites.forEach(function (suiteInfo) { + suite(suiteInfo.title, function () { + setup(function () { + this.field.setValidator(suiteInfo.validator); + }); + test('When Editing', function () { + this.field.isBeingEdited_ = true; + this.field.htmlInput_.value = suiteInfo.value; + this.field.onHtmlInputChange(null); + assertFieldValue( + this.field, + suiteInfo.expectedValue, + suiteInfo.value, + ); + }); + test('When Not Editing', function () { + this.field.setValue(suiteInfo.value); + assertFieldValue(this.field, suiteInfo.expectedValue); + }); + }); + }); + }); + + suite('Customization', function () { + suite('Spellcheck', function () { + setup(function () { + this.prepField = function (field) { + const workspace = { + getAbsoluteScale: function () { + return 1; + }, + getRenderer: function () { + return { + getClassName: function () { + return ''; + }, + }; + }, + getTheme: function () { + return { + getClassName: function () { + return ''; + }, + }; + }, + markFocused: function () {}, + options: {}, + }; + field.sourceBlock_ = { + workspace: workspace, + }; + field.constants_ = { + FIELD_TEXT_FONTSIZE: 11, + FIELD_TEXT_FONTWEIGHT: 'normal', + FIELD_TEXT_FONTFAMILY: 'sans-serif', + }; + field.clickTarget_ = document.createElement('div'); + Blockly.common.setMainWorkspace(workspace); + Blockly.WidgetDiv.createDom(); + this.stub = sinon.stub(field, 'resizeEditor_'); + }; + + this.assertSpellcheck = function (field, value) { + this.prepField(field); + field.showEditor_(); + assert.equal( + field.htmlInput_.getAttribute('spellcheck'), + value.toString(), + ); + }; + }); + teardown(function () { + if (this.stub) { + this.stub.restore(); + } + }); + test('Default', function () { + const field = new Blockly.FieldTextInput('test'); + this.assertSpellcheck(field, true); + }); + test('JS Constructor', function () { + const field = new Blockly.FieldTextInput('test', null, { + spellcheck: false, + }); + this.assertSpellcheck(field, false); + }); + test('JSON Definition', function () { + const field = Blockly.FieldTextInput.fromJson({ + text: 'test', + spellcheck: false, + }); + this.assertSpellcheck(field, false); + }); + test('setSpellcheck Editor Hidden', function () { + const field = new Blockly.FieldTextInput('test'); + field.setSpellcheck(false); + this.assertSpellcheck(field, false); + }); + test('setSpellcheck Editor Shown', function () { + const field = new Blockly.FieldTextInput('test'); + this.prepField(field); + field.showEditor_(); + field.setSpellcheck(false); + assert.equal(field.htmlInput_.getAttribute('spellcheck'), 'false'); + }); + }); + }); + + suite('Serialization', function () { + setup(function () { + this.workspace = new Blockly.Workspace(); + defineRowBlock(); + + this.assertValue = (value) => { + const block = this.workspace.newBlock('row_block'); + const field = new Blockly.FieldTextInput(value); + block.getInput('INPUT').appendField(field, 'TEXT'); + const jso = Blockly.serialization.blocks.save(block); + assert.deepEqual(jso['fields'], {'TEXT': value}); + }; + }); + + teardown(function () { + workspaceTeardown.call(this, this.workspace); + }); + + test('Simple', function () { + this.assertValue('test text'); + }); + }); + + suite('Use editor', function () { + setup(function () { + this.blockJson = { + 'type': 'math_arithmetic', + 'id': 'test_arithmetic_block', + 'fields': { + 'OP': 'ADD', + }, + 'inputs': { + 'A': { + 'shadow': { + 'type': 'math_number', + 'id': 'left_input_block', + 'name': 'test_name', + 'fields': { + 'NUM': 1, + }, + }, + }, + 'B': { + 'shadow': { + 'type': 'math_number', + 'id': 'right_input_block', + 'fields': { + 'NUM': 2, + }, + }, + }, + }, + }; + + this.getFieldFromShadowBlock = function (shadowBlock) { + return shadowBlock.getFields().next().value; + }; + + this.simulateTypingIntoInput = (inputElem, newText) => { + // Typing into an input field changes its value directly and then fires + // an InputEvent (which FieldInput relies on to automatically + // synchronize its state). + inputElem.value = newText; + inputElem.dispatchEvent(new InputEvent('input')); + }; + }); + + // The block being tested doesn't use full-block fields in Geras. + suite('Geras theme', function () { + setup(function () { + this.workspace = Blockly.inject('blocklyDiv', { + renderer: 'geras', + }); + Blockly.serialization.blocks.append(this.blockJson, this.workspace); + + // The workspace actually needs to be visible for focus. + document.getElementById('blocklyDiv').style.visibility = 'visible'; + }); + teardown(function () { + document.getElementById('blocklyDiv').style.visibility = 'hidden'; + workspaceTeardown.call(this, this.workspace); + }); + + test('No editor open by default', function () { + // The editor is only opened if its indicated that it should be open. + assert.isNull(document.querySelector('.blocklyHtmlInput')); + }); + + test('Type in editor with escape does not change field value', async function () { + const block = this.workspace.getBlockById('left_input_block'); + const field = this.getFieldFromShadowBlock(block); + field.showEditor(); + // This must be called to avoid editor resize logic throwing an error. + await Blockly.renderManagement.finishQueuedRenders(); + + // Change the value of the field's input through its editor. + const fieldEditor = document.querySelector('.blocklyHtmlInput'); + this.simulateTypingIntoInput(fieldEditor, 'updated value'); + fieldEditor.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'Escape', + }), + ); + + // 'Escape' will avoid saving the edited field value and close the editor. + assert.equal(field.getValue(), 1); + assert.isNull(document.querySelector('.blocklyHtmlInput')); + }); + + test('Type in editor with enter changes field value', async function () { + const block = this.workspace.getBlockById('left_input_block'); + const field = this.getFieldFromShadowBlock(block); + field.showEditor(); + // This must be called to avoid editor resize logic throwing an error. + await Blockly.renderManagement.finishQueuedRenders(); + + // Change the value of the field's input through its editor. + const fieldEditor = document.querySelector('.blocklyHtmlInput'); + this.simulateTypingIntoInput(fieldEditor, '10'); + fieldEditor.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'Enter', + }), + ); + + // 'Enter' will save the edited result and close the editor. + assert.equal(field.getValue(), 10); + assert.isNull(document.querySelector('.blocklyHtmlInput')); + }); + + test('Not finishing editing does not return ephemeral focus', async function () { + const block = this.workspace.getBlockById('left_input_block'); + const field = this.getFieldFromShadowBlock(block); + Blockly.getFocusManager().focusNode(field); + field.showEditor(); + // This must be called to avoid editor resize logic throwing an error. + await Blockly.renderManagement.finishQueuedRenders(); + + // Change the value of the field's input through its editor. + const fieldEditor = document.querySelector('.blocklyHtmlInput'); + this.simulateTypingIntoInput(fieldEditor, '10'); + + // If the editor doesn't restore focus then the current focused element is + // still the editor. + assert.strictEqual(document.activeElement, fieldEditor); + }); + + test('Finishing editing returns ephemeral focus', async function () { + const block = this.workspace.getBlockById('left_input_block'); + const field = this.getFieldFromShadowBlock(block); + Blockly.getFocusManager().focusNode(field); + field.showEditor(); + // This must be called to avoid editor resize logic throwing an error. + await Blockly.renderManagement.finishQueuedRenders(); + + // Change the value of the field's input through its editor. + const fieldEditor = document.querySelector('.blocklyHtmlInput'); + this.simulateTypingIntoInput(fieldEditor, '10'); + fieldEditor.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'Escape', + }), + ); + + // Verify that exiting the editor restores focus back to the field. + assert.strictEqual(Blockly.getFocusManager().getFocusedNode(), field); + assert.strictEqual(document.activeElement, field.getFocusableElement()); + }); + + test('Opening an editor, tabbing, then editing changes the second field', async function () { + const leftInputBlock = this.workspace.getBlockById('left_input_block'); + const rightInputBlock = + this.workspace.getBlockById('right_input_block'); + const leftField = this.getFieldFromShadowBlock(leftInputBlock); + const rightField = this.getFieldFromShadowBlock(rightInputBlock); + leftField.showEditor(); + // This must be called to avoid editor resize logic throwing an error. + await Blockly.renderManagement.finishQueuedRenders(); + + // Tab, then edit and close the editor. + document.querySelector('.blocklyHtmlInput').dispatchEvent( + new KeyboardEvent('keydown', { + key: 'Tab', + }), + ); + const rightFieldEditor = document.querySelector('.blocklyHtmlInput'); + this.simulateTypingIntoInput(rightFieldEditor, '15'); + rightFieldEditor.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'Enter', + }), + ); + + // Verify that only the right field changed (due to the tab). + assert.equal(leftField.getValue(), 1); + assert.equal(rightField.getValue(), 15); + assert.isNull(document.querySelector('.blocklyHtmlInput')); + }); + + test('Opening an editor, tabbing, then editing changes focus to the second field', async function () { + const leftInputBlock = this.workspace.getBlockById('left_input_block'); + const rightInputBlock = + this.workspace.getBlockById('right_input_block'); + const leftField = this.getFieldFromShadowBlock(leftInputBlock); + const rightField = this.getFieldFromShadowBlock(rightInputBlock); + Blockly.getFocusManager().focusNode(leftField); + leftField.showEditor(); + // This must be called to avoid editor resize logic throwing an error. + await Blockly.renderManagement.finishQueuedRenders(); + + // Tab, then edit and close the editor. + document.querySelector('.blocklyHtmlInput').dispatchEvent( + new KeyboardEvent('keydown', { + key: 'Tab', + }), + ); + const rightFieldEditor = document.querySelector('.blocklyHtmlInput'); + this.simulateTypingIntoInput(rightFieldEditor, '15'); + rightFieldEditor.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'Enter', + }), + ); + + // Verify that the tab causes focus to change to the right field. + assert.strictEqual( + Blockly.getFocusManager().getFocusedNode(), + rightField, + ); + assert.strictEqual( + document.activeElement, + rightField.getFocusableElement(), + ); + }); + }); + + // The block being tested uses full-block fields in Zelos. + suite('Zelos theme', function () { + setup(function () { + this.workspace = Blockly.inject('blocklyDiv', { + renderer: 'zelos', + }); + Blockly.serialization.blocks.append(this.blockJson, this.workspace); + + // The workspace actually needs to be visible for focus. + document.getElementById('blocklyDiv').style.visibility = 'visible'; + }); + teardown(function () { + document.getElementById('blocklyDiv').style.visibility = 'hidden'; + workspaceTeardown.call(this, this.workspace); + }); + + test('Opening an editor, tabbing, then editing full block field changes the second field', async function () { + const leftInputBlock = this.workspace.getBlockById('left_input_block'); + const rightInputBlock = + this.workspace.getBlockById('right_input_block'); + const leftField = this.getFieldFromShadowBlock(leftInputBlock); + const rightField = this.getFieldFromShadowBlock(rightInputBlock); + leftField.showEditor(); + // This must be called to avoid editor resize logic throwing an error. + await Blockly.renderManagement.finishQueuedRenders(); + + // Tab, then edit and close the editor. + document.querySelector('.blocklyHtmlInput').dispatchEvent( + new KeyboardEvent('keydown', { + key: 'Tab', + }), + ); + const rightFieldEditor = document.querySelector('.blocklyHtmlInput'); + this.simulateTypingIntoInput(rightFieldEditor, '15'); + rightFieldEditor.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'Enter', + }), + ); + + // Verify that only the right field changed (due to the tab). + assert.equal(leftField.getValue(), 1); + assert.equal(rightField.getValue(), 15); + assert.isNull(document.querySelector('.blocklyHtmlInput')); + }); + + test('Opening an editor, tabbing, then editing full block field changes focus to the second field', async function () { + const leftInputBlock = this.workspace.getBlockById('left_input_block'); + const rightInputBlock = + this.workspace.getBlockById('right_input_block'); + const leftField = this.getFieldFromShadowBlock(leftInputBlock); + Blockly.getFocusManager().focusNode(leftInputBlock); + leftField.showEditor(); + // This must be called to avoid editor resize logic throwing an error. + await Blockly.renderManagement.finishQueuedRenders(); + + // Tab, then edit and close the editor. + document.querySelector('.blocklyHtmlInput').dispatchEvent( + new KeyboardEvent('keydown', { + key: 'Tab', + }), + ); + const rightFieldEditor = document.querySelector('.blocklyHtmlInput'); + this.simulateTypingIntoInput(rightFieldEditor, '15'); + rightFieldEditor.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'Enter', + }), + ); + + // Verify that the tab causes focus to change to the right field block. + assert.strictEqual( + Blockly.getFocusManager().getFocusedNode(), + rightInputBlock, + ); + assert.strictEqual( + document.activeElement, + rightInputBlock.getFocusableElement(), + ); + }); + }); + }); +}); diff --git a/tests/mocha/field_variable_test.js b/packages/blockly/tests/mocha/field_variable_test.js similarity index 85% rename from tests/mocha/field_variable_test.js rename to packages/blockly/tests/mocha/field_variable_test.js index 78dad10bac3..270a662cf0f 100644 --- a/tests/mocha/field_variable_test.js +++ b/packages/blockly/tests/mocha/field_variable_test.js @@ -5,7 +5,7 @@ */ import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { createTestBlock, defineRowBlock, @@ -171,7 +171,7 @@ suite('Variable Fields', function () { suite('setValue', function () { setup(function () { - this.workspace.createVariable('name2', null, 'id2'); + this.workspace.getVariableMap().createVariable('name2', null, 'id2'); this.field = new Blockly.FieldVariable(null); initVariableField(this.workspace, this.field); @@ -209,8 +209,8 @@ suite('Variable Fields', function () { assert.include(dropdownOptions[dropdownOptions.length - 1][0], 'Delete'); }; test('Contains variables created before field', function () { - this.workspace.createVariable('name1', '', 'id1'); - this.workspace.createVariable('name2', '', 'id2'); + this.workspace.getVariableMap().createVariable('name1', '', 'id1'); + this.workspace.getVariableMap().createVariable('name2', '', 'id2'); // Expect that the dropdown options will contain the variables that exist const fieldVariable = initVariableField( this.workspace, @@ -228,22 +228,22 @@ suite('Variable Fields', function () { new Blockly.FieldVariable('name1'), ); // Expect that variables created after field creation will show up too. - this.workspace.createVariable('name2', '', 'id2'); + this.workspace.getVariableMap().createVariable('name2', '', 'id2'); assertDropdownContents(fieldVariable, [ ['name1', 'id1'], ['name2', 'id2'], ]); }); test('Contains variables created before and after field', function () { - this.workspace.createVariable('name1', '', 'id1'); - this.workspace.createVariable('name2', '', 'id2'); + this.workspace.getVariableMap().createVariable('name1', '', 'id1'); + this.workspace.getVariableMap().createVariable('name2', '', 'id2'); // Expect that the dropdown options will contain the variables that exist const fieldVariable = initVariableField( this.workspace, new Blockly.FieldVariable('name1'), ); // Expect that variables created after field creation will show up too. - this.workspace.createVariable('name3', '', 'id3'); + this.workspace.getVariableMap().createVariable('name3', '', 'id3'); assertDropdownContents(fieldVariable, [ ['name1', 'id1'], ['name2', 'id2'], @@ -254,9 +254,9 @@ suite('Variable Fields', function () { suite('Validators', function () { setup(function () { - this.workspace.createVariable('name1', null, 'id1'); - this.workspace.createVariable('name2', null, 'id2'); - this.workspace.createVariable('name3', null, 'id3'); + this.workspace.getVariableMap().createVariable('name1', null, 'id1'); + this.workspace.getVariableMap().createVariable('name2', null, 'id2'); + this.workspace.getVariableMap().createVariable('name3', null, 'id3'); this.variableField = initVariableField( this.workspace, new Blockly.FieldVariable('name1'), @@ -281,7 +281,9 @@ suite('Variable Fields', function () { }); test('New Value', function () { // Must create the var so that the field doesn't throw an error. - this.workspace.createVariable('thing2', null, 'other2'); + this.workspace + .getVariableMap() + .createVariable('thing2', null, 'other2'); this.variableField.setValue('other2'); assertFieldValue(this.variableField, 'id2', 'name2'); }); @@ -309,6 +311,24 @@ suite('Variable Fields', function () { assert.deepEqual(field.variableTypes, ['Type1']); assert.equal(field.defaultType, 'Type1'); }); + test('Empty list of types', function () { + assert.throws(function () { + const fieldVariable = new Blockly.FieldVariable( + 'name1', + undefined, + [], + ); + }); + }); + test('invalid value for list of types', function () { + assert.throws(function () { + const fieldVariable = new Blockly.FieldVariable( + 'name1', + undefined, + 'not an array', + ); + }); + }); test('JSON Definition', function () { const field = Blockly.FieldVariable.fromJson({ variable: 'test', @@ -350,15 +370,8 @@ suite('Variable Fields', function () { }); suite('Get variable types', function () { setup(function () { - this.workspace.createVariable('name1', 'type1'); - this.workspace.createVariable('name2', 'type2'); - }); - test('variableTypes is undefined', function () { - // Expect that since variableTypes is undefined, only type empty string - // will be returned (regardless of what types are available on the workspace). - const fieldVariable = new Blockly.FieldVariable('name1'); - const resultTypes = fieldVariable.getVariableTypes(); - assert.deepEqual(resultTypes, ['']); + this.workspace.getVariableMap().createVariable('name1', 'type1'); + this.workspace.getVariableMap().createVariable('name2', 'type2'); }); test('variableTypes is explicit', function () { // Expect that since variableTypes is defined, it will be the return @@ -377,6 +390,17 @@ suite('Variable Fields', function () { 'Default type was wrong', ); }); + test('variableTypes is undefined', function () { + // Expect all variable types in the workspace to be returned, same + // as if variableTypes is null. + const fieldVariable = new Blockly.FieldVariable('name1'); + const mockBlock = createTestBlock(); + mockBlock.workspace = this.workspace; + fieldVariable.setSourceBlock(mockBlock); + + const resultTypes = fieldVariable.getVariableTypes(); + assert.deepEqual(resultTypes, ['type1', 'type2']); + }); test('variableTypes is null', function () { // Expect all variable types to be returned. // The field does not need to be initialized to do this--it just needs @@ -388,19 +412,25 @@ suite('Variable Fields', function () { fieldVariable.variableTypes = null; const resultTypes = fieldVariable.getVariableTypes(); - // The empty string is always one of the options. - assert.deepEqual(resultTypes, ['type1', 'type2', '']); + assert.deepEqual(resultTypes, ['type1', 'type2']); }); - test('variableTypes is the empty list', function () { - const fieldVariable = new Blockly.FieldVariable('name1'); + test('variableTypes is null and variable is in the flyout', function () { + // Expect all variable types in the workspace and + // flyout workspace to be returned. + const fieldVariable = new Blockly.FieldVariable('name1', undefined, null); const mockBlock = createTestBlock(); mockBlock.workspace = this.workspace; + + // Pretend this is a flyout workspace with potential variables + mockBlock.isInFlyout = true; + mockBlock.workspace.createPotentialVariableMap(); + mockBlock.workspace + .getPotentialVariableMap() + .createVariable('name3', 'type3'); fieldVariable.setSourceBlock(mockBlock); - fieldVariable.variableTypes = []; - assert.throws(function () { - fieldVariable.getVariableTypes(); - }); + const resultTypes = fieldVariable.getVariableTypes(); + assert.deepEqual(resultTypes, ['type1', 'type2', 'type3']); }); }); suite('Default types', function () { @@ -439,7 +469,7 @@ suite('Variable Fields', function () { }); suite('Renaming Variables', function () { setup(function () { - this.workspace.createVariable('name1', null, 'id1'); + this.workspace.getVariableMap().createVariable('name1', null, 'id1'); Blockly.defineBlocksWithJsonArray([ { 'type': 'field_variable_test_block', @@ -460,13 +490,15 @@ suite('Variable Fields', function () { this.variableField = this.variableBlock.getField('VAR'); }); test('Rename & Keep Old ID', function () { - this.workspace.renameVariableById('id1', 'name2'); + const variableMap = this.workspace.getVariableMap(); + variableMap.renameVariable(variableMap.getVariableById('id1'), 'name2'); assert.equal(this.variableField.getText(), 'name2'); assert.equal(this.variableField.getValue(), 'id1'); }); test('Rename & Get New ID', function () { - this.workspace.createVariable('name2', null, 'id2'); - this.workspace.renameVariableById('id1', 'name2'); + const variableMap = this.workspace.getVariableMap(); + variableMap.createVariable('name2', null, 'id2'); + variableMap.renameVariable(variableMap.getVariableById('id1'), 'name2'); assert.equal(this.variableField.getText(), 'name2'); assert.equal(this.variableField.getValue(), 'id2'); }); @@ -554,7 +586,7 @@ suite('Variable Fields', function () { }); test('ID', function () { - this.workspace.createVariable('test', '', 'id1'); + this.workspace.getVariableMap().createVariable('test', '', 'id1'); const block = Blockly.serialization.blocks.append( { 'type': 'variables_get', diff --git a/tests/mocha/flyout_test.js b/packages/blockly/tests/mocha/flyout_test.js similarity index 92% rename from tests/mocha/flyout_test.js rename to packages/blockly/tests/mocha/flyout_test.js index 9be45458c51..e2812b25ba3 100644 --- a/tests/mocha/flyout_test.js +++ b/packages/blockly/tests/mocha/flyout_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, @@ -43,6 +43,26 @@ suite('Flyout', function () { sharedTestTeardown.call(this); }); + suite('workspace change listeners', function () { + test('are triggered when a child block changes', function () { + let listenerTriggered = false; + const listener = (e) => { + if (e.type === Blockly.Events.BLOCK_CHANGE) { + listenerTriggered = true; + } + }; + this.workspace.getFlyout().getWorkspace().addChangeListener(listener); + const boolBlock = this.workspace + .getFlyout() + .getWorkspace() + .getBlocksByType('logic_compare')[0]; + boolBlock.getField('OP').setValue('LTE'); + this.clock.tick(1000); + assert.isTrue(listenerTriggered); + this.workspace.getFlyout().getWorkspace().removeChangeListener(listener); + }); + }); + suite('position', function () { suite('vertical flyout', function () { suite('simple flyout', function () { @@ -309,16 +329,12 @@ suite('Flyout', function () { function checkFlyoutInfo(flyoutSpy) { const flyoutInfo = flyoutSpy.returnValues[0]; - const contents = flyoutInfo.contents; - const gaps = flyoutInfo.gaps; - - const expectedGaps = [20, 24, 24]; - assert.deepEqual(gaps, expectedGaps); + const contents = flyoutInfo; - assert.equal(contents.length, 3, 'Contents'); + assert.equal(contents.length, 6, 'Contents'); assert.equal(contents[0].type, 'block', 'Contents'); - const block = contents[0]['block']; + const block = contents[0]['element']; assert.instanceOf(block, Blockly.BlockSvg); assert.equal(block.getFieldValue('OP'), 'NEQ'); const childA = block.getInputTargetBlock('A'); @@ -328,11 +344,20 @@ suite('Flyout', function () { assert.equal(childA.getFieldValue('NUM'), 1); assert.equal(childB.getFieldValue('NUM'), 2); - assert.equal(contents[1].type, 'button', 'Contents'); - assert.instanceOf(contents[1]['button'], Blockly.FlyoutButton); + assert.equal(contents[1].type, 'sep'); + assert.equal(contents[1].element.getBoundingRectangle().getHeight(), 20); assert.equal(contents[2].type, 'button', 'Contents'); - assert.instanceOf(contents[2]['button'], Blockly.FlyoutButton); + assert.instanceOf(contents[2]['element'], Blockly.FlyoutButton); + + assert.equal(contents[3].type, 'sep'); + assert.equal(contents[3].element.getBoundingRectangle().getHeight(), 24); + + assert.equal(contents[4].type, 'label', 'Contents'); + assert.instanceOf(contents[4]['element'], Blockly.FlyoutButton); + + assert.equal(contents[5].type, 'sep'); + assert.equal(contents[5].element.getBoundingRectangle().getHeight(), 24); } suite('Direct show', function () { @@ -621,35 +646,5 @@ suite('Flyout', function () { const block = this.flyout.workspace_.getAllBlocks()[0]; assert.equal(block.getFieldValue('NUM'), 321); }); - - test('Recycling enabled', function () { - this.flyout.blockIsRecyclable_ = function () { - return true; - }; - this.flyout.show({ - 'contents': [ - { - 'kind': 'BLOCK', - 'type': 'math_number', - 'fields': { - 'NUM': 123, - }, - }, - ], - }); - this.flyout.show({ - 'contents': [ - { - 'kind': 'BLOCK', - 'type': 'math_number', - 'fields': { - 'NUM': 321, - }, - }, - ], - }); - const block = this.flyout.workspace_.getAllBlocks()[0]; - assert.equal(block.getFieldValue('NUM'), 123); - }); }); }); diff --git a/packages/blockly/tests/mocha/focus_manager_test.js b/packages/blockly/tests/mocha/focus_manager_test.js new file mode 100644 index 00000000000..490fa4301b8 --- /dev/null +++ b/packages/blockly/tests/mocha/focus_manager_test.js @@ -0,0 +1,5979 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + FocusManager, + getFocusManager, +} from '../../build/src/core/focus_manager.js'; +import {assert} from '../../node_modules/chai/index.js'; +import { + sharedTestSetup, + sharedTestTeardown, +} from './test_helpers/setup_teardown.js'; + +class FocusableNodeImpl { + constructor(element, tree) { + this.element = element; + this.tree = tree; + } + + getFocusableElement() { + return this.element; + } + + getFocusableTree() { + return this.tree; + } + + onNodeFocus() {} + + onNodeBlur() {} + + canBeFocused() { + return true; + } +} + +class FocusableTreeImpl { + constructor(rootElement, nestedTrees) { + this.nestedTrees = nestedTrees; + this.idToNodeMap = {}; + this.rootNode = this.addNode(rootElement); + this.fallbackNode = null; + } + + addNode(element) { + const node = new FocusableNodeImpl(element, this); + this.idToNodeMap[element.id] = node; + return node; + } + + removeNode(node) { + delete this.idToNodeMap[node.getFocusableElement().id]; + } + + getRootFocusableNode() { + return this.rootNode; + } + + getRestoredFocusableNode() { + return this.fallbackNode; + } + + getNestedTrees() { + return this.nestedTrees; + } + + lookUpFocusableNode(id) { + return this.idToNodeMap[id]; + } + + onTreeFocus() {} + + onTreeBlur() {} +} + +suite('FocusManager', function () { + const ACTIVE_FOCUS_NODE_CSS_SELECTOR = `.${FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME}`; + const PASSIVE_FOCUS_NODE_CSS_SELECTOR = `.${FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME}`; + + setup(function () { + sharedTestSetup.call(this); + this.focusManager = getFocusManager(); + + this.allFocusableTrees = []; + this.allFocusableNodes = []; + this.createFocusableTree = function (rootElementId, nestedTrees) { + const tree = new FocusableTreeImpl( + document.getElementById(rootElementId), + nestedTrees || [], + ); + this.allFocusableTrees.push(tree); + return tree; + }; + this.createFocusableNode = function (tree, elementId) { + const node = tree.addNode(document.getElementById(elementId)); + this.allFocusableNodes.push(node); + return node; + }; + + this.testFocusableTree1 = this.createFocusableTree('testFocusableTree1'); + this.testFocusableTree1Node1 = this.createFocusableNode( + this.testFocusableTree1, + 'testFocusableTree1.node1', + ); + this.testFocusableTree1Node1Child1 = this.createFocusableNode( + this.testFocusableTree1, + 'testFocusableTree1.node1.child1', + ); + this.testFocusableTree1Node2 = this.createFocusableNode( + this.testFocusableTree1, + 'testFocusableTree1.node2', + ); + this.testFocusableNestedTree4 = this.createFocusableTree( + 'testFocusableNestedTree4', + ); + this.testFocusableNestedTree4Node1 = this.createFocusableNode( + this.testFocusableNestedTree4, + 'testFocusableNestedTree4.node1', + ); + this.testFocusableNestedTree5 = this.createFocusableTree( + 'testFocusableNestedTree5', + ); + this.testFocusableNestedTree5Node1 = this.createFocusableNode( + this.testFocusableNestedTree5, + 'testFocusableNestedTree5.node1', + ); + this.testFocusableTree2 = this.createFocusableTree('testFocusableTree2', [ + this.testFocusableNestedTree4, + this.testFocusableNestedTree5, + ]); + this.testFocusableTree2Node1 = this.createFocusableNode( + this.testFocusableTree2, + 'testFocusableTree2.node1', + ); + + this.testFocusableGroup1 = this.createFocusableTree('testFocusableGroup1'); + this.testFocusableGroup1Node1 = this.createFocusableNode( + this.testFocusableGroup1, + 'testFocusableGroup1.node1', + ); + this.testFocusableGroup1Node1Child1 = this.createFocusableNode( + this.testFocusableGroup1, + 'testFocusableGroup1.node1.child1', + ); + this.testFocusableGroup1Node2 = this.createFocusableNode( + this.testFocusableGroup1, + 'testFocusableGroup1.node2', + ); + this.testFocusableNestedGroup4 = this.createFocusableTree( + 'testFocusableNestedGroup4', + ); + this.testFocusableNestedGroup4Node1 = this.createFocusableNode( + this.testFocusableNestedGroup4, + 'testFocusableNestedGroup4.node1', + ); + this.testFocusableGroup2 = this.createFocusableTree('testFocusableGroup2', [ + this.testFocusableNestedGroup4, + ]); + this.testFocusableGroup2Node1 = this.createFocusableNode( + this.testFocusableGroup2, + 'testFocusableGroup2.node1', + ); + }); + + teardown(function () { + sharedTestTeardown.call(this); + + // Ensure all node CSS styles are reset so that state isn't leaked between tests. + const activeElems = document.querySelectorAll( + ACTIVE_FOCUS_NODE_CSS_SELECTOR, + ); + const passiveElems = document.querySelectorAll( + PASSIVE_FOCUS_NODE_CSS_SELECTOR, + ); + for (const elem of activeElems) { + elem.classList.remove(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + } + for (const elem of passiveElems) { + elem.classList.remove(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + } + + // Ensure any set tab indexes are properly reset between tests. + for (const tree of this.allFocusableTrees) { + tree + .getRootFocusableNode() + .getFocusableElement() + .removeAttribute('tabindex'); + } + for (const node of this.allFocusableNodes) { + node.getFocusableElement().removeAttribute('tabindex'); + } + this.allFocusableTrees = []; + this.allFocusableNodes = []; + + // Reset the current active element. + document.body.focus(); + }); + + assert.includesClass = function (classList, className) { + assert.isTrue( + classList.contains(className), + 'Expected class list to include: ' + className, + ); + }; + + assert.notIncludesClass = function (classList, className) { + assert.isFalse( + classList.contains(className), + 'Expected class list to not include: ' + className, + ); + }; + + /* Basic lifecycle tests. */ + + suite('registerTree()', function () { + test('once does not throw', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + // The test should pass due to no exception being thrown. + }); + + test('twice for same tree throws error', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + const errorMsgRegex = + /Attempted to re-register already registered tree.+?/; + assert.throws( + () => this.focusManager.registerTree(this.testFocusableTree1), + errorMsgRegex, + ); + }); + + test('twice with different trees does not throw', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableGroup1); + + // The test shouldn't throw since two different trees were registered. + }); + + test('register after an unregister does not throw', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.unregisterTree(this.testFocusableTree1); + + this.focusManager.registerTree(this.testFocusableTree1); + + // The second register should not fail since the tree was previously unregistered. + }); + + test('for tree with missing ID throws error', function () { + const rootNode = this.testFocusableTree1.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + const oldId = rootElem.id; + rootElem.removeAttribute('id'); + + const errorMsgRegex = + /Attempting to register a tree with a root element that has an invalid ID.+?/; + assert.throws( + () => this.focusManager.registerTree(this.testFocusableTree1), + errorMsgRegex, + ); + // Restore the ID for other tests. + rootElem.id = oldId; + }); + + test('for tree with null ID throws error', function () { + const rootNode = this.testFocusableTree1.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + const oldId = rootElem.id; + rootElem.setAttribute('id', null); + + const errorMsgRegex = + /Attempting to register a tree with a root element that has an invalid ID.+?/; + assert.throws( + () => this.focusManager.registerTree(this.testFocusableTree1), + errorMsgRegex, + ); + // Restore the ID for other tests. + rootElem.id = oldId; + }); + + test('for tree with empty throws error', function () { + const rootNode = this.testFocusableTree1.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + const oldId = rootElem.id; + rootElem.setAttribute('id', ''); + + const errorMsgRegex = + /Attempting to register a tree with a root element that has an invalid ID.+?/; + assert.throws( + () => this.focusManager.registerTree(this.testFocusableTree1), + errorMsgRegex, + ); + // Restore the ID for other tests. + rootElem.id = oldId; + }); + + test('for unmanaged tree does not overwrite tab index', function () { + this.focusManager.registerTree(this.testFocusableTree1, false); + + const rootNode = this.testFocusableTree1.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + assert.isNull(rootElem.getAttribute('tabindex')); + }); + + test('for unmanaged tree with custom tab index does not overwrite tab index', function () { + const rootNode = this.testFocusableTree1.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + rootElem.tabIndex = -1; + + this.focusManager.registerTree(this.testFocusableTree1, false); + + // The custom tab index shouldn't be overwritten for an unmanaged tree. + assert.strictEqual(rootElem.getAttribute('tabindex'), '-1'); + }); + + test('for managed tree overwrites root tab index to be tab navigable', function () { + this.focusManager.registerTree(this.testFocusableTree1, true); + + const rootNode = this.testFocusableTree1.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + assert.strictEqual(rootElem.getAttribute('tabindex'), '0'); + }); + + test('for managed tree with custom tab index overwrites root tab index to be tab navigable', function () { + const rootNode = this.testFocusableTree1.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + rootElem.tabIndex = -1; + + this.focusManager.registerTree(this.testFocusableTree1, true); + + // A custom tab index should be overwritten for a managed tree. + assert.strictEqual(rootElem.getAttribute('tabindex'), '0'); + }); + }); + + suite('unregisterTree()', function () { + test('for not yet registered tree throws', function () { + const errorMsgRegex = /Attempted to unregister not registered tree.+?/; + assert.throws( + () => this.focusManager.unregisterTree(this.testFocusableTree1), + errorMsgRegex, + ); + }); + + test('for registered tree does not throw', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Unregistering a registered tree should not fail. + }); + + test('twice for registered tree throws', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.unregisterTree(this.testFocusableTree1); + + const errorMsgRegex = /Attempted to unregister not registered tree.+?/; + assert.throws( + () => this.focusManager.unregisterTree(this.testFocusableTree1), + errorMsgRegex, + ); + }); + + test('for unmanaged tree with custom tab index does not change tab index', function () { + this.focusManager.registerTree(this.testFocusableTree1, false); + const rootNode = this.testFocusableTree1.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + rootElem.tabIndex = -1; + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Unregistering an unmanaged tree shouldn't change its tab index. + assert.strictEqual(rootElem.getAttribute('tabindex'), '-1'); + }); + + test('for managed tree removes tab index', function () { + this.focusManager.registerTree(this.testFocusableTree1, true); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Unregistering a managed tree should remove its tab index. + const rootNode = this.testFocusableTree1.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + assert.isNull(rootElem.getAttribute('tabindex')); + }); + + test('for managed tree with custom tab index removes tab index', function () { + this.focusManager.registerTree(this.testFocusableTree1, true); + const rootNode = this.testFocusableTree1.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + rootElem.tabIndex = -1; + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Unregistering a managed tree should remove its tab index. + assert.isNull(rootElem.getAttribute('tabindex')); + }); + }); + + suite('isRegistered()', function () { + test('for not registered tree returns false', function () { + const isRegistered = this.focusManager.isRegistered( + this.testFocusableTree1, + ); + + assert.isFalse(isRegistered); + }); + + test('for registered tree returns true', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + const isRegistered = this.focusManager.isRegistered( + this.testFocusableTree1, + ); + + assert.isTrue(isRegistered); + }); + + test('for unregistered tree returns false', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.unregisterTree(this.testFocusableTree1); + + const isRegistered = this.focusManager.isRegistered( + this.testFocusableTree1, + ); + + assert.isFalse(isRegistered); + }); + + test('for re-registered tree returns true', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.unregisterTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree1); + + const isRegistered = this.focusManager.isRegistered( + this.testFocusableTree1, + ); + + assert.isTrue(isRegistered); + }); + + test('for unregistered tree with other registered tree returns false', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.unregisterTree(this.testFocusableTree1); + + const isRegistered = this.focusManager.isRegistered( + this.testFocusableTree1, + ); + + assert.isFalse(isRegistered); + }); + }); + + suite('getFocusedTree()', function () { + test('by default returns null', function () { + const focusedTree = this.focusManager.getFocusedTree(); + + assert.isNull(focusedTree); + }); + }); + + suite('getFocusedNode()', function () { + test('by default returns null', function () { + const focusedNode = this.focusManager.getFocusedNode(); + + assert.isNull(focusedNode); + }); + + test('after focusing unfocusable node returns null', function () { + this.testFocusableTree1Node1.canBeFocused = () => false; + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + const focusedNode = this.focusManager.getFocusedNode(); + + // Unfocusable nodes should not be focused. + assert.isNull(focusedNode); + }); + }); + + suite('focusTree()', function () { + test('for not registered tree throws', function () { + const errorMsgRegex = /Attempted to focus unregistered tree.+?/; + assert.throws( + () => this.focusManager.focusTree(this.testFocusableTree1), + errorMsgRegex, + ); + }); + + test('for unregistered tree throws', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.unregisterTree(this.testFocusableTree1); + + const errorMsgRegex = /Attempted to focus unregistered tree.+?/; + assert.throws( + () => this.focusManager.focusTree(this.testFocusableTree1), + errorMsgRegex, + ); + }); + }); + + test('unfocused node does not have a tab index by default', function () { + const elem = this.testFocusableTree1Node1.getFocusableElement(); + + // This is slightly testing the test setup, but it acts as a precondition sanity test for the + // other tab index tests below. Important: 'getAttribute' is used here since direct access to + // 'tabIndex' can default the value returned even when the tab index isn't set. + assert.isNull(elem.getAttribute('tabindex')); + }); + + suite('focusNode()', function () { + test('for not registered node throws', function () { + const errorMsgRegex = /Attempted to focus unregistered node.+?/; + assert.throws( + () => this.focusManager.focusNode(this.testFocusableTree1Node1), + errorMsgRegex, + ); + }); + + test('for unregistered node throws', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.unregisterTree(this.testFocusableTree1); + + const errorMsgRegex = /Attempted to focus unregistered node.+?/; + assert.throws( + () => this.focusManager.focusNode(this.testFocusableTree1Node1), + errorMsgRegex, + ); + }); + + test('focuses element', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusNode(this.testFocusableTree1Node1); + + const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); + assert.strictEqual(document.activeElement, nodeElem); + }); + + test('fires focusin event', function () { + let focusCount = 0; + const focusListener = () => focusCount++; + document.addEventListener('focusin', focusListener); + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusNode(this.testFocusableTree1Node1); + document.removeEventListener('focusin', focusListener); + + // There should be exactly 1 focus event fired from focusNode(). + assert.strictEqual(focusCount, 1); + }); + + test('for orphaned node returns tree root by default', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.testFocusableTree1.removeNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode(this.testFocusableTree1Node1); + + // Focusing an invalid node should fall back to the tree root when it has no restoration + // fallback node. + const currentNode = this.focusManager.getFocusedNode(); + const treeRoot = this.testFocusableTree1.getRootFocusableNode(); + assert.strictEqual(currentNode, treeRoot); + }); + + test('for orphaned node returns specified fallback node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.testFocusableTree1.fallbackNode = this.testFocusableTree1Node2; + this.testFocusableTree1.removeNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode(this.testFocusableTree1Node1); + + // Focusing an invalid node should fall back to the restored fallback. + const currentNode = this.focusManager.getFocusedNode(); + assert.strictEqual(currentNode, this.testFocusableTree1Node2); + }); + + test('restores focus when element quietly loses focus', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + // Remove the FocusManager's listeners to simulate not receiving a focus + // event when focus is lost. This can happen in Firefox and Safari when an + // element is removed and then re-added to the DOM. This is a contrived + // setup to achieve the same outcome on all browsers. For context, see: + // https://github.com/google/blockly-keyboard-experimentation/issues/87. + for (const registeredListener of this.globalDocumentEventListeners) { + const eventType = registeredListener.type; + const eventListener = registeredListener.listener; + document.removeEventListener(eventType, eventListener); + } + document.body.focus(); + + this.focusManager.focusNode(this.testFocusableTree1Node1); + + const currentNode = this.focusManager.getFocusedNode(); + const currentElem = currentNode?.getFocusableElement(); + assert.strictEqual(currentNode, this.testFocusableTree1Node1); + assert.strictEqual(document.activeElement, currentElem); + }); + + test('restores focus when element and new node focused', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + // Remove the FocusManager's listeners to simulate not receiving a focus + // event when focus is lost. This can happen in Firefox and Safari when an + // element is removed and then re-added to the DOM. This is a contrived + // setup to achieve the same outcome on all browsers. For context, see: + // https://github.com/google/blockly-keyboard-experimentation/issues/87. + for (const registeredListener of this.globalDocumentEventListeners) { + const eventType = registeredListener.type; + const eventListener = registeredListener.listener; + document.removeEventListener(eventType, eventListener); + } + document.body.focus(); + + this.focusManager.focusNode(this.testFocusableTree1Node2); + + const currentNode = this.focusManager.getFocusedNode(); + const currentElem = currentNode?.getFocusableElement(); + assert.strictEqual(currentNode, this.testFocusableTree1Node2); + assert.strictEqual(document.activeElement, currentElem); + }); + + test('for unfocused node calls onNodeFocus once', function () { + sinon.spy(this.testFocusableTree1Node1, 'onNodeFocus'); + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusNode(this.testFocusableTree1Node1); + + assert.strictEqual(this.testFocusableTree1Node1.onNodeFocus.callCount, 1); + }); + + test('for previously focused node calls onNodeBlur once', function () { + sinon.spy(this.testFocusableTree1Node1, 'onNodeBlur'); + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode(this.testFocusableTree1Node2); + + assert.strictEqual(this.testFocusableTree1Node1.onNodeBlur.callCount, 1); + }); + + test('for unfocused tree calls onTreeFocus once', function () { + sinon.spy(this.testFocusableTree1, 'onTreeFocus'); + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusNode(this.testFocusableTree1Node1); + + assert.strictEqual(this.testFocusableTree1.onTreeFocus.callCount, 1); + }); + + test('for previously focused tree calls onTreeBlur once', function () { + sinon.spy(this.testFocusableTree1, 'onTreeBlur'); + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode(this.testFocusableTree2Node1); + + assert.strictEqual(this.testFocusableTree1.onTreeBlur.callCount, 1); + }); + + test('for same node twice calls onNodeFocus once', function () { + sinon.spy(this.testFocusableTree1Node1, 'onNodeFocus'); + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + // Call focus for the same node a second time. + this.focusManager.focusNode(this.testFocusableTree1Node1); + + // Despite two calls to focus the node should only focus once. + assert.strictEqual(this.testFocusableTree1Node1.onNodeFocus.callCount, 1); + }); + + test('for unfocusable node does not call onNodeFocus', function () { + sinon.spy(this.testFocusableTree1Node1, 'onNodeFocus'); + this.testFocusableTree1Node1.canBeFocused = () => false; + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusNode(this.testFocusableTree1Node1); + + // Unfocusable nodes should not be focused, nor have their callbacks called. + assert.strictEqual(this.testFocusableTree1Node1.onNodeFocus.callCount, 0); + }); + + test('for unfocused node overwrites tab index', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusNode(this.testFocusableTree1Node1); + + // Focusing an element should overwrite its tab index. + const elem = this.testFocusableTree1Node1.getFocusableElement(); + assert.strictEqual(elem.getAttribute('tabindex'), '-1'); + }); + + test('for previously focused node keeps new tab index', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode(this.testFocusableTree1Node2); + + // The previously focused element should retain its tab index. + const elem = this.testFocusableTree1Node1.getFocusableElement(); + assert.strictEqual(elem.getAttribute('tabindex'), '-1'); + }); + + test('for node with custom tab index does not change tab index', function () { + this.focusManager.registerTree(this.testFocusableTree1); + const elem = this.testFocusableTree1Node1.getFocusableElement(); + elem.tabIndex = 0; + + this.focusManager.focusNode(this.testFocusableTree1Node1); + + // If the node already has a tab index set then it should retain that index. + assert.strictEqual(elem.getAttribute('tabindex'), '0'); + }); + + suite('for unmanaged tree', function () { + test('focused root overwrites tab index', function () { + this.focusManager.registerTree(this.testFocusableTree1, false); + const rootNode = this.testFocusableTree1.getRootFocusableNode(); + + this.focusManager.focusNode(rootNode); + + // Focusing an unmanaged tree's root should overwrite its tab index. + const rootElem = rootNode.getFocusableElement(); + assert.strictEqual(rootElem.getAttribute('tabindex'), '-1'); + }); + + test('focused root with custom tab index does not change tab index', function () { + this.focusManager.registerTree(this.testFocusableTree1, false); + const rootNode = this.testFocusableTree1.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + rootElem.tabIndex = 0; + + this.focusManager.focusNode(rootNode); + + // If the node already has a tab index set then it should retain that index. + assert.strictEqual(rootElem.getAttribute('tabindex'), '0'); + }); + + test('focused node in a tree after unmanaged was focused should keep previous root unchanged', function () { + this.focusManager.registerTree(this.testFocusableTree1, false); + this.focusManager.registerTree(this.testFocusableTree2, false); + const rootNode = this.testFocusableTree1.getRootFocusableNode(); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode(this.testFocusableTree2Node1); + + // Focusing a different tree shouldn't change the root of the previous tree if it's unmanaged. + const rootElem = rootNode.getFocusableElement(); + assert.isNull(rootElem.getAttribute('tabindex')); + }); + + test('focused node in a tree after unmanaged was root focused should make previous root tab navigable', function () { + this.focusManager.registerTree(this.testFocusableTree1, false); + this.focusManager.registerTree(this.testFocusableTree2, false); + const rootNode = this.testFocusableTree1.getRootFocusableNode(); + this.focusManager.focusTree(this.testFocusableTree1); + + this.focusManager.focusNode(this.testFocusableTree2Node1); + + // The previous tree's root should be kept unchanged (since it was managed). + const rootElem = rootNode.getFocusableElement(); + assert.strictEqual(rootElem.getAttribute('tabindex'), '-1'); + }); + }); + + suite('for managed tree', function () { + test('for unfocused node in managed tree overwrites tab index', function () { + this.focusManager.registerTree(this.testFocusableTree1, true); + + this.focusManager.focusNode(this.testFocusableTree1Node1); + + // Focusing an element should overwrite its tab index. + const elem = this.testFocusableTree1Node1.getFocusableElement(); + assert.strictEqual(elem.getAttribute('tabindex'), '-1'); + }); + + test('for previously focused node in managed tree keeps new tab index', function () { + this.focusManager.registerTree(this.testFocusableTree1, true); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode(this.testFocusableTree1Node2); + + // The previously focused element should retain its tab index. + const elem = this.testFocusableTree1Node1.getFocusableElement(); + assert.strictEqual(elem.getAttribute('tabindex'), '-1'); + }); + + test('focused root makes root non-tab navigable', function () { + this.focusManager.registerTree(this.testFocusableTree1, true); + const rootNode = this.testFocusableTree1.getRootFocusableNode(); + + this.focusManager.focusNode(rootNode); + + // Focusing the root in a managed tree should make it non-tab navigable. + const rootElem = rootNode.getFocusableElement(); + assert.strictEqual(rootElem.getAttribute('tabindex'), '-1'); + }); + + test('focused root with custom tab index should overwrite tab index', function () { + this.focusManager.registerTree(this.testFocusableTree1, true); + const rootNode = this.testFocusableTree1.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + rootElem.tabIndex = 0; + + this.focusManager.focusNode(rootNode); + + // Custom tab indexes are overwritten for the root in a managed tree. + assert.strictEqual(rootElem.getAttribute('tabindex'), '-1'); + }); + + test('focused node tree root makes root non-tab navigable', function () { + this.focusManager.registerTree(this.testFocusableTree1, true); + const rootNode = this.testFocusableTree1.getRootFocusableNode(); + + this.focusManager.focusNode(this.testFocusableTree1Node1); + + // Focusing a node of a managed tree should make the root non-tab navigable. + const rootElem = rootNode.getFocusableElement(); + assert.strictEqual(rootElem.getAttribute('tabindex'), '-1'); + }); + + test('focused node root with custom tab index should overwrite tab index', function () { + this.focusManager.registerTree(this.testFocusableTree1, true); + const rootNode = this.testFocusableTree1.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + rootElem.tabIndex = 0; + + this.focusManager.focusNode(this.testFocusableTree1Node1); + + // Custom tab indexes are overwritten for the root in a managed tree even when a tree's node + // is focused. + assert.strictEqual(rootElem.getAttribute('tabindex'), '-1'); + }); + + test('focused node in a tree after managed was focused should make previous root tab navigable', function () { + this.focusManager.registerTree(this.testFocusableTree1, true); + this.focusManager.registerTree(this.testFocusableTree2, false); + const rootNode = this.testFocusableTree1.getRootFocusableNode(); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode(this.testFocusableTree2Node1); + + // Focusing a different tree shouldn't after a managed tree should make the managed tree tab + // navigable. + const rootElem = rootNode.getFocusableElement(); + assert.strictEqual(rootElem.getAttribute('tabindex'), '0'); + }); + + test('focused node in a tree after managed was root focused should make previous root tab navigable', function () { + this.focusManager.registerTree(this.testFocusableTree1, true); + this.focusManager.registerTree(this.testFocusableTree2, false); + const rootNode = this.testFocusableTree1.getRootFocusableNode(); + this.focusManager.focusTree(this.testFocusableTree1); + + this.focusManager.focusNode(this.testFocusableTree2Node1); + + // Focusing a different tree shouldn't after a managed tree should make the managed tree tab + // navigable. + const rootElem = rootNode.getFocusableElement(); + assert.strictEqual(rootElem.getAttribute('tabindex'), '0'); + }); + }); + }); + + suite('getFocusManager()', function () { + test('returns non-null manager', function () { + const manager = getFocusManager(); + + assert.isNotNull(manager); + }); + + test('returns the exact same instance in subsequent calls', function () { + const manager1 = getFocusManager(); + const manager2 = getFocusManager(); + + assert.strictEqual(manager2, manager1); + }); + }); + + /* Focus tests for HTML trees. */ + + suite('focus*() switching in HTML tree', function () { + suite('getFocusedTree()', function () { + test('registered tree focusTree()ed no prev focus returns tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusTree(this.testFocusableTree1); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree1, + ); + }); + + test('registered tree focusTree()ed prev node focused returns tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusTree(this.testFocusableTree1); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree1, + ); + }); + + test('registered tree focusTree()ed diff tree prev focused returns new tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusTree(this.testFocusableTree1); + + this.focusManager.focusTree(this.testFocusableTree2); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + }); + + test('registered tree focusTree()ed diff tree node prev focused returns new tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusTree(this.testFocusableTree2); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + }); + + test('registered root focusNode()ed no prev focus returns tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusNode( + this.testFocusableTree1.getRootFocusableNode(), + ); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree1, + ); + }); + + test("registered node focusNode()ed no prev focus returns node's tree", function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusNode(this.testFocusableTree1Node1); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree1, + ); + }); + + test("registered subnode focusNode()ed no prev focus returns node's tree", function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusNode(this.testFocusableTree1Node1Child1); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree1, + ); + }); + + test('registered node focusNode()ed after prev node focus returns same tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode(this.testFocusableTree1Node2); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree1, + ); + }); + + test("registered node focusNode()ed after prev node focus diff tree returns new node's tree", function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode(this.testFocusableTree2Node1); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + }); + + test("registered tree root focusNode()ed after prev node focus diff tree returns new node's tree", function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode( + this.testFocusableTree2.getRootFocusableNode(), + ); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + }); + + test('unregistered tree focusTree()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusTree(this.testFocusableTree1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focusNode()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focusNode()ed with prev node prior focused returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the more recent tree was removed, there's no tree currently focused. + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focusNode()ed with prev node recently focused returns new tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the most recent tree still exists, it still has focus. + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + }); + + test('nested tree focusTree()ed with no prev focus returns nested tree', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + + this.focusManager.focusTree(this.testFocusableNestedTree4); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableNestedTree4, + ); + }); + + test('nested tree node focusNode()ed with no prev focus returns nested tree', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + + this.focusManager.focusNode(this.testFocusableNestedTree4Node1); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableNestedTree4, + ); + }); + + test('nested tree node focusNode()ed after parent focused returns nested tree', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + this.focusManager.focusNode(this.testFocusableNestedTree4Node1); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableNestedTree4, + ); + }); + }); + suite('getFocusedNode()', function () { + test('registered tree focusTree()ed no prev focus returns root node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusTree(this.testFocusableTree1); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree1.getRootFocusableNode(), + ); + }); + + test('registered tree focusTree()ed prev node focused returns original node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusTree(this.testFocusableTree1); + + // The original node retains focus since the tree already holds focus (per focusTree's + // contract). + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree1Node1, + ); + }); + + test('registered tree focusTree()ed diff tree prev focused returns new root node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusTree(this.testFocusableTree1); + + this.focusManager.focusTree(this.testFocusableTree2); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree2.getRootFocusableNode(), + ); + }); + + test('registered tree focusTree()ed diff tree node prev focused returns new root node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusTree(this.testFocusableTree2); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree2.getRootFocusableNode(), + ); + }); + + test('registered root focusNode()ed no prev focus returns root node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusNode( + this.testFocusableTree1.getRootFocusableNode(), + ); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree1.getRootFocusableNode(), + ); + }); + + test('registered node focusNode()ed no prev focus returns node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusNode(this.testFocusableTree1Node1); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree1Node1, + ); + }); + + test('registered subnode focusNode()ed no prev focus returns subnode', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusNode(this.testFocusableTree1Node1Child1); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree1Node1Child1, + ); + }); + + test('registered node focusNode()ed after prev node focus returns new node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode(this.testFocusableTree1Node2); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree1Node2, + ); + }); + + test('registered node focusNode()ed after prev node focus diff tree returns new node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode(this.testFocusableTree2Node1); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + }); + + test('registered tree root focusNode()ed after prev node focus diff tree returns new root', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode( + this.testFocusableTree2.getRootFocusableNode(), + ); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree2.getRootFocusableNode(), + ); + }); + + test('unregistered tree focusTree()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusTree(this.testFocusableTree1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unregistered tree focusNode()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unregistered tree focusNode()ed with prev node prior focused returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the more recent tree was removed, there's no tree currently focused. + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unregistered tree focusNode()ed with prev node recently focused returns new node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the most recent tree still exists, it still has focus. + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + }); + + test('nested tree focusTree()ed with no prev focus returns nested root', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + + this.focusManager.focusTree(this.testFocusableNestedTree4); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableNestedTree4.getRootFocusableNode(), + ); + }); + + test('nested tree node focusNode()ed with no prev focus returns focused node', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + + this.focusManager.focusNode(this.testFocusableNestedTree4Node1); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableNestedTree4Node1, + ); + }); + + test('nested tree node focusNode()ed after parent focused returns focused node', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + this.focusManager.focusNode(this.testFocusableNestedTree4Node1); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableNestedTree4Node1, + ); + }); + + test('deletion after focusNode() returns null', function () { + const rootElem = document.createElement('div'); + const nodeElem = document.createElement('div'); + rootElem.setAttribute('id', 'focusRoot'); + rootElem.setAttribute('tabindex', '-1'); + nodeElem.setAttribute('id', 'focusNode'); + nodeElem.setAttribute('tabindex', '-1'); + nodeElem.textContent = 'Focusable node'; + rootElem.appendChild(nodeElem); + document.body.appendChild(rootElem); + const root = this.createFocusableTree('focusRoot'); + const node = this.createFocusableNode(root, 'focusNode'); + this.focusManager.registerTree(root); + this.focusManager.focusNode(node); + + node.getFocusableElement().remove(); + + assert.notStrictEqual(this.focusManager.getFocusedNode(), node); + rootElem.remove(); // Cleanup. + }); + }); + suite('CSS classes', function () { + test('registered tree focusTree()ed no prev focus root elem has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusTree(this.testFocusableTree1); + + const rootElem = this.testFocusableTree1 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered tree focusTree()ed prev node focused original elem has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusTree(this.testFocusableTree1); + + // The original node retains active focus since the tree already holds focus (per + // focusTree's contract). + const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered tree focusTree()ed diff tree prev focused new root elem has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusTree(this.testFocusableTree1); + + this.focusManager.focusTree(this.testFocusableTree2); + + const rootElem = this.testFocusableTree2 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered tree focusTree()ed diff tree node prev focused new root elem has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusTree(this.testFocusableTree2); + + const rootElem = this.testFocusableTree2 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered root focusNode()ed no prev focus returns root elem has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusNode( + this.testFocusableTree1.getRootFocusableNode(), + ); + + const rootElem = this.testFocusableTree1 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focusNode()ed no prev focus node elem has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusNode(this.testFocusableTree1Node1); + + const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focusNode()ed after prev node focus same tree old node elem has no focus property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode(this.testFocusableTree1Node2); + + const prevNodeElem = this.testFocusableTree1Node1.getFocusableElement(); + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focusNode()ed after prev node focus same tree new node elem has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode(this.testFocusableTree1Node2); + + const newNodeElem = this.testFocusableTree1Node2.getFocusableElement(); + assert.includesClass( + newNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + newNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focusNode()ed after prev node focus diff tree old node elem has passive property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode(this.testFocusableTree2Node1); + + const prevNodeElem = this.testFocusableTree1Node1.getFocusableElement(); + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focusNode()ed after prev node focus diff tree new node elem has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode(this.testFocusableTree2Node1); + + const newNodeElem = this.testFocusableTree2Node1.getFocusableElement(); + assert.includesClass( + newNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + newNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered tree root focusNode()ed after prev node focus diff tree new root has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode( + this.testFocusableTree2.getRootFocusableNode(), + ); + + const rootElem = this.testFocusableTree2 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unregistered tree focusTree()ed with no prev focus removes focus', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusTree(this.testFocusableTree1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the tree was unregistered it no longer has focus indicators. + const rootElem = this.testFocusableTree1 + .getRootFocusableNode() + .getFocusableElement(); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unregistered tree focusNode()ed with no prev focus removes focus', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the tree was unregistered it no longer has focus indicators. + const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unregistered tree focusNode()ed with prev node prior removes focus from removed tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the tree was unregistered it no longer has focus indicators. However, the old node + // should still have passive indication. + const otherNodeElem = + this.testFocusableTree2Node1.getFocusableElement(); + const removedNodeElem = + this.testFocusableTree1Node1.getFocusableElement(); + assert.includesClass( + otherNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + otherNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unregistered tree focusNode()ed with prev node recently removes focus from removed tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the tree was unregistered it no longer has focus indicators. However, the new node + // should still have active indication. + const otherNodeElem = + this.testFocusableTree2Node1.getFocusableElement(); + const removedNodeElem = + this.testFocusableTree1Node1.getFocusableElement(); + assert.includesClass( + otherNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + otherNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('focusNode() multiple nodes in same tree with switches ensure passive focus has gone', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + this.focusManager.focusNode(this.testFocusableTree1Node2); + + // When switching back to the first tree, ensure the original passive node is no longer + // passive now that the new node is active. + const node1 = this.testFocusableTree1Node1.getFocusableElement(); + const node2 = this.testFocusableTree1Node2.getFocusableElement(); + assert.notIncludesClass( + node1.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + node2.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered tree focusTree()ed other tree node passively focused tree node now has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + this.focusManager.focusTree(this.testFocusableTree1); + + // The original node in the tree should be moved from passive to active focus per the + // contract of focusTree). Also, the root of the tree should have no focus indication. + const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); + const rootElem = this.testFocusableTree1 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('focus on root, node in diff tree, then node in first tree; root should have focus gone', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + this.focusManager.focusNode(this.testFocusableTree1Node1); + + const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); + const rootElem = this.testFocusableTree1 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('nested tree focusTree()ed with no prev root has active focus', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + + this.focusManager.focusTree(this.testFocusableNestedTree4); + + const rootElem = this.testFocusableNestedTree4 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('nested tree node focusNode()ed with no prev focus node has active focus', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + + this.focusManager.focusNode(this.testFocusableNestedTree4Node1); + + const nodeElem = + this.testFocusableNestedTree4Node1.getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('nested tree node focusNode()ed after parent focused prev has passive node has active', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + this.focusManager.focusNode(this.testFocusableNestedTree4Node1); + + const prevNodeElem = this.testFocusableTree2Node1.getFocusableElement(); + const currNodeElem = + this.testFocusableNestedTree4Node1.getFocusableElement(); + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + currNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + }); + }); + + suite('DOM focus() switching in HTML tree', function () { + suite('getFocusedTree()', function () { + test('registered root focus()ed no prev focus returns tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1').tabIndex = -1; + + document.getElementById('testFocusableTree1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree1, + ); + }); + + test("registered node focus()ed no prev focus returns node's tree", function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + + document.getElementById('testFocusableTree1.node1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree1, + ); + }); + + test("registered subnode focus()ed no prev focus returns node's tree", function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1.child1').tabIndex = + -1; + + document.getElementById('testFocusableTree1.node1.child1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree1, + ); + }); + + test('registered node focus()ed after prev node focus returns same tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + document.getElementById('testFocusableTree1.node2').tabIndex = -1; + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testFocusableTree1.node2').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree1, + ); + }); + + test("registered node focus()ed after prev node focus diff tree returns new node's tree", function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + document.getElementById('testFocusableTree2.node1').tabIndex = -1; + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testFocusableTree2.node1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + }); + + test("registered tree root focus()ed after prev node focus diff tree returns new node's tree", function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + document.getElementById('testFocusableTree2').tabIndex = -1; + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testFocusableTree2').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + }); + + test("non-registered node subelement focus()ed returns node's tree", function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById( + 'testFocusableTree1.node2.unregisteredChild1', + ).tabIndex = -1; + + document + .getElementById('testFocusableTree1.node2.unregisteredChild1') + .focus(); + + // The tree of the unregistered child element should take focus. + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree1, + ); + }); + + test('non-registered tree focus()ed returns null', function () { + document.getElementById('testUnregisteredFocusableTree3').tabIndex = -1; + + document.getElementById('testUnregisteredFocusableTree3').focus(); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('non-registered tree node focus()ed returns null', function () { + document.getElementById( + 'testUnregisteredFocusableTree3.node1', + ).tabIndex = -1; + + document.getElementById('testUnregisteredFocusableTree3.node1').focus(); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('non-registered tree node focus()ed after registered node focused returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + document.getElementById( + 'testUnregisteredFocusableTree3.node1', + ).tabIndex = -1; + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testUnregisteredFocusableTree3.node1').focus(); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unfocusable element focus()ed after registered node focused returns original tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testUnfocusableElement').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree1, + ); + }); + + test('unregistered tree focus()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1').tabIndex = -1; + document.getElementById('testFocusableTree1').focus(); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focus()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + document.getElementById('testFocusableTree1.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focus()ed with prev node prior focused returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree2.node1').tabIndex = -1; + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + document.getElementById('testFocusableTree2.node1').focus(); + document.getElementById('testFocusableTree1.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the more recent tree was removed, there's no tree currently focused. + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focus()ed with prev node recently focused returns new tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + document.getElementById('testFocusableTree2.node1').tabIndex = -1; + document.getElementById('testFocusableTree1.node1').focus(); + document.getElementById('testFocusableTree2.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the most recent tree still exists, it still has focus. + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + }); + + test('unregistered tree focus()ed with prev node after unregistering returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + document.getElementById('testFocusableTree2.node1').tabIndex = -1; + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + document.getElementById('testFocusableTree1.node1').focus(); + document.getElementById('testFocusableTree2.node1').focus(); + this.focusManager.unregisterTree(this.testFocusableTree1); + + document.getElementById('testFocusableTree1.node1').focus(); + + // Attempting to focus a now removed tree should result in nothing being + // focused since the removed tree can have DOM focus, but that focus is + // ignored by FocusManager. + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('nested tree focusTree()ed with no prev focus returns nested tree', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + document.getElementById('testFocusableNestedTree4').tabIndex = -1; + + document.getElementById('testFocusableNestedTree4').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableNestedTree4, + ); + }); + + test('nested tree node focusNode()ed with no prev focus returns nested tree', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + document.getElementById('testFocusableNestedTree4.node1').tabIndex = -1; + + document.getElementById('testFocusableNestedTree4.node1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableNestedTree4, + ); + }); + + test('nested tree node focusNode()ed after parent focused returns nested tree', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + document.getElementById('testFocusableTree2.node1').tabIndex = -1; + document.getElementById('testFocusableNestedTree4.node1').tabIndex = -1; + document.getElementById('testFocusableTree2.node1').focus(); + + document.getElementById('testFocusableNestedTree4.node1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableNestedTree4, + ); + }); + }); + suite('getFocusedNode()', function () { + test('registered root focus()ed no prev focus returns root node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1').tabIndex = -1; + + document.getElementById('testFocusableTree1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree1.getRootFocusableNode(), + ); + }); + + test('registered node focus()ed no prev focus returns node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + + document.getElementById('testFocusableTree1.node1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree1Node1, + ); + }); + + test('registered subnode focus()ed no prev focus returns subnode', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1.child1').tabIndex = + -1; + + document.getElementById('testFocusableTree1.node1.child1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree1Node1Child1, + ); + }); + + test('registered node focus()ed after prev node focus returns new node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + document.getElementById('testFocusableTree1.node2').tabIndex = -1; + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testFocusableTree1.node2').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree1Node2, + ); + }); + + test('registered node focus()ed after prev node focus diff tree returns new node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + document.getElementById('testFocusableTree2.node1').tabIndex = -1; + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testFocusableTree2.node1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + }); + + test('registered tree root focus()ed after prev node focus diff tree returns new root', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + document.getElementById('testFocusableTree2').tabIndex = -1; + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testFocusableTree2').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree2.getRootFocusableNode(), + ); + }); + + test('non-registered node subelement focus()ed returns nearest node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById( + 'testFocusableTree1.node2.unregisteredChild1', + ).tabIndex = -1; + + document + .getElementById('testFocusableTree1.node2.unregisteredChild1') + .focus(); + + // The nearest node of the unregistered child element should take focus. + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree1Node2, + ); + }); + + test('non-registered tree focus()ed returns null', function () { + document.getElementById('testUnregisteredFocusableTree3').tabIndex = -1; + + document.getElementById('testUnregisteredFocusableTree3').focus(); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('non-registered tree node focus()ed returns null', function () { + document.getElementById( + 'testUnregisteredFocusableTree3.node1', + ).tabIndex = -1; + + document.getElementById('testUnregisteredFocusableTree3.node1').focus(); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('non-registered tree node focus()ed after registered node focused returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + document.getElementById( + 'testUnregisteredFocusableTree3.node1', + ).tabIndex = -1; + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testUnregisteredFocusableTree3.node1').focus(); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unfocusable element focus()ed after registered node focused returns original node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testUnfocusableElement').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree1Node1, + ); + }); + + test('unregistered tree focus()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1').tabIndex = -1; + document.getElementById('testFocusableTree1').focus(); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unregistered tree focus()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + document.getElementById('testFocusableTree1.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unregistered tree focus()ed with prev node prior focused returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree2.node1').tabIndex = -1; + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + document.getElementById('testFocusableTree2.node1').focus(); + document.getElementById('testFocusableTree1.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the more recent tree was removed, there's no tree currently focused. + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unregistered tree focus()ed with prev node recently focused returns new node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + document.getElementById('testFocusableTree2.node1').tabIndex = -1; + document.getElementById('testFocusableTree1.node1').focus(); + document.getElementById('testFocusableTree2.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the most recent tree still exists, it still has focus. + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + }); + + test('unregistered tree focus()ed with prev node after unregistering returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + document.getElementById('testFocusableTree2.node1').tabIndex = -1; + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + document.getElementById('testFocusableTree1.node1').focus(); + document.getElementById('testFocusableTree2.node1').focus(); + this.focusManager.unregisterTree(this.testFocusableTree1); + + document.getElementById('testFocusableTree1.node1').focus(); + + // Attempting to focus a now removed tree should result in nothing being + // focused since the removed tree can have DOM focus, but that focus is + // ignored by FocusManager. + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('nested tree focus()ed with no prev focus returns nested root', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + document.getElementById('testFocusableNestedTree4').tabIndex = -1; + + document.getElementById('testFocusableNestedTree4').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableNestedTree4.getRootFocusableNode(), + ); + }); + + test('nested tree node focus()ed with no prev focus returns focused node', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + document.getElementById('testFocusableNestedTree4.node1').tabIndex = -1; + + document.getElementById('testFocusableNestedTree4.node1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableNestedTree4Node1, + ); + }); + + test('nested tree node focus()ed after parent focused returns focused node', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + document.getElementById('testFocusableTree2.node1').tabIndex = -1; + document.getElementById('testFocusableNestedTree4.node1').tabIndex = -1; + document.getElementById('testFocusableTree2.node1').focus(); + + document.getElementById('testFocusableNestedTree4.node1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableNestedTree4Node1, + ); + }); + + test('deletion after focus() returns null', function () { + const rootElem = document.createElement('div'); + const nodeElem = document.createElement('div'); + rootElem.setAttribute('id', 'focusRoot'); + rootElem.setAttribute('tabindex', '-1'); + nodeElem.setAttribute('id', 'focusNode'); + nodeElem.setAttribute('tabindex', '-1'); + nodeElem.textContent = 'Focusable node'; + rootElem.appendChild(nodeElem); + document.body.appendChild(rootElem); + const root = this.createFocusableTree('focusRoot'); + const node = this.createFocusableNode(root, 'focusNode'); + this.focusManager.registerTree(root); + document.getElementById('focusNode').tabIndex = -1; + document.getElementById('focusNode').focus(); + + node.getFocusableElement().remove(); + + assert.notStrictEqual(this.focusManager.getFocusedNode(), node); + rootElem.remove(); // Cleanup. + }); + + test('after focus() after trying to focusNode() an unfocusable node updates returns focus()ed node', function () { + this.testFocusableTree1Node1.canBeFocused = () => false; + document.getElementById('testFocusableTree1.node2').tabIndex = -1; + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + document.getElementById('testFocusableTree1.node2').focus(); + + // focus()ing a new node should overwrite a failed attempt to focusNode() an unfocusable + // node. This verifies that DOM focus syncing is properly reenabled by FocusManager. + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree1Node2, + ); + }); + + test('after focus() after trying to focusNode() the same node twice returns focus()ed node', function () { + document.getElementById('testFocusableTree1.node2').tabIndex = -1; + this.focusManager.registerTree(this.testFocusableTree1); + // Intentionally try to focus the same node twice. + this.focusManager.focusNode(this.testFocusableTree1Node1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + document.getElementById('testFocusableTree1.node2').focus(); + + // focus()ing a new node should overwrite a failed attempt to focusNode() the same node + // twice. This verifies that DOM focus syncing is properly reenabled by FocusManager. + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree1Node2, + ); + }); + }); + suite('CSS classes', function () { + test('registered root focus()ed no prev focus returns root elem has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1').tabIndex = -1; + + document.getElementById('testFocusableTree1').focus(); + + const rootElem = this.testFocusableTree1 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focus()ed no prev focus node elem has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + + document.getElementById('testFocusableTree1.node1').focus(); + + const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focus()ed after prev node focus same tree old node elem has no focus property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + document.getElementById('testFocusableTree1.node2').tabIndex = -1; + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testFocusableTree1.node2').focus(); + + const prevNodeElem = this.testFocusableTree1Node1.getFocusableElement(); + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focus()ed after prev node focus same tree new node elem has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + document.getElementById('testFocusableTree1.node2').tabIndex = -1; + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testFocusableTree1.node2').focus(); + + const newNodeElem = this.testFocusableTree1Node2.getFocusableElement(); + assert.includesClass( + newNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + newNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focus()ed after prev node focus diff tree old node elem has passive property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + document.getElementById('testFocusableTree2.node1').tabIndex = -1; + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testFocusableTree2.node1').focus(); + + const prevNodeElem = this.testFocusableTree1Node1.getFocusableElement(); + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focus()ed after prev node focus diff tree new node elem has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + document.getElementById('testFocusableTree2.node1').tabIndex = -1; + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testFocusableTree2.node1').focus(); + + const newNodeElem = this.testFocusableTree2Node1.getFocusableElement(); + assert.includesClass( + newNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + newNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered tree root focus()ed after prev node focus diff tree new root has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + document.getElementById('testFocusableTree2').tabIndex = -1; + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testFocusableTree2').focus(); + + const rootElem = this.testFocusableTree2 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('non-registered node subelement focus()ed nearest node has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById( + 'testFocusableTree1.node2.unregisteredChild1', + ).tabIndex = -1; + + document + .getElementById('testFocusableTree1.node2.unregisteredChild1') + .focus(); + + // The nearest node of the unregistered child element should be actively focused. + const nodeElem = this.testFocusableTree1Node2.getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('non-registered tree focus()ed has no focus', function () { + document.getElementById('testUnregisteredFocusableTree3').tabIndex = -1; + + document.getElementById('testUnregisteredFocusableTree3').focus(); + + assert.isNull(this.focusManager.getFocusedNode()); + const rootElem = document.getElementById( + 'testUnregisteredFocusableTree3', + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('non-registered tree node focus()ed has no focus', function () { + document.getElementById( + 'testUnregisteredFocusableTree3.node1', + ).tabIndex = -1; + + document.getElementById('testUnregisteredFocusableTree3.node1').focus(); + + assert.isNull(this.focusManager.getFocusedNode()); + const nodeElem = document.getElementById( + 'testUnregisteredFocusableTree3.node1', + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unfocsable element focus()ed after registered node focused original node has active focus', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testUnfocusableElement').focus(); + + // The original node should be unchanged, and the unregistered node should not have any + // focus indicators. + const nodeElem = document.getElementById('testFocusableTree1.node1'); + const attemptedNewNodeElem = document.getElementById( + 'testUnfocusableElement', + ); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + attemptedNewNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + attemptedNewNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unregistered tree focus()ed with no prev focus removes focus', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1').tabIndex = -1; + document.getElementById('testFocusableTree1').focus(); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the tree was unregistered it no longer has focus indicators. + const rootElem = this.testFocusableTree1 + .getRootFocusableNode() + .getFocusableElement(); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unregistered tree focus()ed with no prev focus removes focus', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + document.getElementById('testFocusableTree1.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the tree was unregistered it no longer has focus indicators. + const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unregistered tree focus()ed with prev node prior removes focus from removed tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree2.node1').tabIndex = -1; + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + document.getElementById('testFocusableTree2.node1').focus(); + document.getElementById('testFocusableTree1.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the tree was unregistered it no longer has focus indicators. However, the old node + // should still have passive indication. + const otherNodeElem = + this.testFocusableTree2Node1.getFocusableElement(); + const removedNodeElem = + this.testFocusableTree1Node1.getFocusableElement(); + assert.includesClass( + otherNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + otherNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unregistered tree focus()ed with prev node recently removes focus from removed tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + document.getElementById('testFocusableTree2.node1').tabIndex = -1; + document.getElementById('testFocusableTree1.node1').focus(); + document.getElementById('testFocusableTree2.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the tree was unregistered it no longer has focus indicators. However, the new node + // should still have active indication. + const otherNodeElem = + this.testFocusableTree2Node1.getFocusableElement(); + const removedNodeElem = + this.testFocusableTree1Node1.getFocusableElement(); + assert.includesClass( + otherNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + otherNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unregistered tree focus()ed with prev node after unregistering removes active indicator', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + document.getElementById('testFocusableTree2.node1').tabIndex = -1; + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + document.getElementById('testFocusableTree1.node1').focus(); + document.getElementById('testFocusableTree2.node1').focus(); + this.focusManager.unregisterTree(this.testFocusableTree1); + + document.getElementById('testFocusableTree1.node1').focus(); + + // Attempting to focus a now removed tree should remove active. + const otherNodeElem = + this.testFocusableTree2Node1.getFocusableElement(); + const removedNodeElem = + this.testFocusableTree1Node1.getFocusableElement(); + assert.notIncludesClass( + otherNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + otherNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('focus() multiple nodes in same tree with switches ensure passive focus has gone', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + document.getElementById('testFocusableTree2.node1').tabIndex = -1; + document.getElementById('testFocusableTree1.node2').tabIndex = -1; + document.getElementById('testFocusableTree1.node1').focus(); + document.getElementById('testFocusableTree2.node1').focus(); + + document.getElementById('testFocusableTree1.node2').focus(); + + // When switching back to the first tree, ensure the original passive node is no longer + // passive now that the new node is active. + const node1 = this.testFocusableTree1Node1.getFocusableElement(); + const node2 = this.testFocusableTree1Node2.getFocusableElement(); + assert.notIncludesClass( + node1.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + node2.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered tree focus()ed other tree node passively focused tree node now has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + document.getElementById('testFocusableTree2.node1').tabIndex = -1; + document.getElementById('testFocusableTree1').tabIndex = -1; + document.getElementById('testFocusableTree1.node1').focus(); + document.getElementById('testFocusableTree2.node1').focus(); + + document.getElementById('testFocusableTree1').focus(); + + // Directly refocusing a tree's root should have functional parity with focusTree(). That + // means the tree's previous node should now have active focus again and its root should + // have no focus indication. + const rootElem = this.testFocusableTree1 + .getRootFocusableNode() + .getFocusableElement(); + const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('focus on root, node in diff tree, then node in first tree; root should have focus gone', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1').tabIndex = -1; + document.getElementById('testFocusableTree2.node1').tabIndex = -1; + document.getElementById('testFocusableTree1.node1').tabIndex = -1; + document.getElementById('testFocusableTree1').focus(); + document.getElementById('testFocusableTree2.node1').focus(); + + document.getElementById('testFocusableTree1.node1').focus(); + + const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); + const rootElem = this.testFocusableTree1 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('nested tree focus()ed with no prev root has active focus', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + document.getElementById('testFocusableNestedTree4').tabIndex = -1; + + document.getElementById('testFocusableNestedTree4').focus(); + + const rootElem = this.testFocusableNestedTree4 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('nested tree node focus()ed with no prev focus node has active focus', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + document.getElementById('testFocusableNestedTree4.node1').tabIndex = -1; + + document.getElementById('testFocusableNestedTree4.node1').focus(); + + const nodeElem = + this.testFocusableNestedTree4Node1.getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('nested tree node focus()ed after parent focused prev has passive node has active', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + document.getElementById('testFocusableTree2.node1').tabIndex = -1; + document.getElementById('testFocusableNestedTree4.node1').tabIndex = -1; + document.getElementById('testFocusableTree2.node1').focus(); + + document.getElementById('testFocusableNestedTree4.node1').focus(); + + const prevNodeElem = this.testFocusableTree2Node1.getFocusableElement(); + const currNodeElem = + this.testFocusableNestedTree4Node1.getFocusableElement(); + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + currNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + }); + }); + + /* Focus tests for SVG trees. */ + + suite('focus*() switching in SVG tree', function () { + suite('getFocusedTree()', function () { + test('registered tree focusTree()ed no prev focus returns tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + this.focusManager.focusTree(this.testFocusableGroup1); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup1, + ); + }); + + test('registered tree focusTree()ed prev node focused returns tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusTree(this.testFocusableGroup1); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup1, + ); + }); + + test('registered tree focusTree()ed diff tree prev focused returns new tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusTree(this.testFocusableGroup1); + + this.focusManager.focusTree(this.testFocusableGroup2); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + }); + + test('registered tree focusTree()ed diff tree node prev focused returns new tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusTree(this.testFocusableGroup2); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + }); + + test('registered root focusNode()ed no prev focus returns tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + this.focusManager.focusNode( + this.testFocusableGroup1.getRootFocusableNode(), + ); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup1, + ); + }); + + test("registered node focusNode()ed no prev focus returns node's tree", function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup1, + ); + }); + + test("registered subnode focusNode()ed no prev focus returns node's tree", function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + this.focusManager.focusNode(this.testFocusableGroup1Node1Child1); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup1, + ); + }); + + test('registered node focusNode()ed after prev node focus returns same tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusNode(this.testFocusableGroup1Node2); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup1, + ); + }); + + test("registered node focusNode()ed after prev node focus diff tree returns new node's tree", function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + }); + + test("registered tree root focusNode()ed after prev node focus diff tree returns new node's tree", function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusNode( + this.testFocusableGroup2.getRootFocusableNode(), + ); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + }); + + test('unregistered tree focusTree()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusTree(this.testFocusableGroup1); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focusNode()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focusNode()ed with prev node prior focused returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the more recent tree was removed, there's no tree currently focused. + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focusNode()ed with prev node recently focused returns new tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the most recent tree still exists, it still has focus. + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + }); + + test('nested tree focusTree()ed with no prev focus returns nested tree', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + + this.focusManager.focusTree(this.testFocusableNestedGroup4); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableNestedGroup4, + ); + }); + + test('nested tree node focusNode()ed with no prev focus returns nested tree', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + + this.focusManager.focusNode(this.testFocusableNestedGroup4Node1); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableNestedGroup4, + ); + }); + + test('nested tree node focusNode()ed after parent focused returns nested tree', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + this.focusManager.focusNode(this.testFocusableNestedGroup4Node1); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableNestedGroup4, + ); + }); + }); + suite('getFocusedNode()', function () { + test('registered tree focusTree()ed no prev focus returns root node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + this.focusManager.focusTree(this.testFocusableGroup1); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1.getRootFocusableNode(), + ); + }); + + test('registered tree focusTree()ed prev node focused returns original node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusTree(this.testFocusableGroup1); + + // The original node retains focus since the tree already holds focus (per focusTree's + // contract). + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1Node1, + ); + }); + + test('registered tree focusTree()ed diff tree prev focused returns new root node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusTree(this.testFocusableGroup1); + + this.focusManager.focusTree(this.testFocusableGroup2); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2.getRootFocusableNode(), + ); + }); + + test('registered tree focusTree()ed diff tree node prev focused returns new root node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusTree(this.testFocusableGroup2); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2.getRootFocusableNode(), + ); + }); + + test('registered root focusNode()ed no prev focus returns root node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + this.focusManager.focusNode( + this.testFocusableGroup1.getRootFocusableNode(), + ); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1.getRootFocusableNode(), + ); + }); + + test('registered node focusNode()ed no prev focus returns node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1Node1, + ); + }); + + test('registered subnode focusNode()ed no prev focus returns subnode', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + this.focusManager.focusNode(this.testFocusableGroup1Node1Child1); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1Node1Child1, + ); + }); + + test('registered node focusNode()ed after prev node focus returns new node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusNode(this.testFocusableGroup1Node2); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1Node2, + ); + }); + + test('registered node focusNode()ed after prev node focus diff tree returns new node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + }); + + test('registered tree root focusNode()ed after prev node focus diff tree returns new root', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusNode( + this.testFocusableGroup2.getRootFocusableNode(), + ); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2.getRootFocusableNode(), + ); + }); + + test('unregistered tree focusTree()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusTree(this.testFocusableGroup1); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unregistered tree focusNode()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unregistered tree focusNode()ed with prev node prior focused returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the more recent tree was removed, there's no tree currently focused. + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unregistered tree focusNode()ed with prev node recently focused returns new node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the most recent tree still exists, it still has focus. + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + }); + + test('nested tree focusTree()ed with no prev focus returns nested root', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + + this.focusManager.focusTree(this.testFocusableNestedGroup4); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableNestedGroup4.getRootFocusableNode(), + ); + }); + + test('nested tree node focusNode()ed with no prev focus returns focused node', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + + this.focusManager.focusNode(this.testFocusableNestedGroup4Node1); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableNestedGroup4Node1, + ); + }); + + test('nested tree node focusNode()ed after parent focused returns focused node', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + this.focusManager.focusNode(this.testFocusableNestedGroup4Node1); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableNestedGroup4Node1, + ); + }); + }); + suite('CSS classes', function () { + test('registered tree focusTree()ed no prev focus root elem has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + this.focusManager.focusTree(this.testFocusableGroup1); + + const rootElem = this.testFocusableGroup1 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered tree focusTree()ed prev node focused original elem has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusTree(this.testFocusableGroup1); + + // The original node retains active focus since the tree already holds focus (per + // focusTree's contract). + const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered tree focusTree()ed diff tree prev focused new root elem has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusTree(this.testFocusableGroup1); + + this.focusManager.focusTree(this.testFocusableGroup2); + + const rootElem = this.testFocusableGroup2 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered tree focusTree()ed diff tree node prev focused new root elem has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusTree(this.testFocusableGroup2); + + const rootElem = this.testFocusableGroup2 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered root focusNode()ed no prev focus returns root elem has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + this.focusManager.focusNode( + this.testFocusableGroup1.getRootFocusableNode(), + ); + + const rootElem = this.testFocusableGroup1 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focusNode()ed no prev focus node elem has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focusNode()ed after prev node focus same tree old node elem has no focus property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusNode(this.testFocusableGroup1Node2); + + const prevNodeElem = + this.testFocusableGroup1Node1.getFocusableElement(); + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focusNode()ed after prev node focus same tree new node elem has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusNode(this.testFocusableGroup1Node2); + + const newNodeElem = this.testFocusableGroup1Node2.getFocusableElement(); + assert.includesClass( + newNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + newNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focusNode()ed after prev node focus diff tree old node elem has passive property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + const prevNodeElem = + this.testFocusableGroup1Node1.getFocusableElement(); + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focusNode()ed after prev node focus diff tree new node elem has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + const newNodeElem = this.testFocusableGroup2Node1.getFocusableElement(); + assert.includesClass( + newNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + newNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered tree root focusNode()ed after prev node focus diff tree new root has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusNode( + this.testFocusableGroup2.getRootFocusableNode(), + ); + + const rootElem = this.testFocusableGroup2 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unregistered tree focusTree()ed with no prev focus removes focus', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusTree(this.testFocusableGroup1); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the tree was unregistered it no longer has focus indicators. + const rootElem = this.testFocusableGroup1 + .getRootFocusableNode() + .getFocusableElement(); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unregistered tree focusNode()ed with no prev focus removes focus', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the tree was unregistered it no longer has focus indicators. + const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unregistered tree focusNode()ed with prev node prior removes focus from removed tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the tree was unregistered it no longer has focus indicators. However, the old node + // should still have passive indication. + const otherNodeElem = + this.testFocusableGroup2Node1.getFocusableElement(); + const removedNodeElem = + this.testFocusableGroup1Node1.getFocusableElement(); + assert.includesClass( + otherNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + otherNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unregistered tree focusNode()ed with prev node recently removes focus from removed tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the tree was unregistered it no longer has focus indicators. However, the new node + // should still have active indication. + const otherNodeElem = + this.testFocusableGroup2Node1.getFocusableElement(); + const removedNodeElem = + this.testFocusableGroup1Node1.getFocusableElement(); + assert.includesClass( + otherNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + otherNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('focusNode() multiple nodes in same tree with switches ensure passive focus has gone', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + this.focusManager.focusNode(this.testFocusableGroup1Node2); + + // When switching back to the first tree, ensure the original passive node is no longer + // passive now that the new node is active. + const node1 = this.testFocusableGroup1Node1.getFocusableElement(); + const node2 = this.testFocusableGroup1Node2.getFocusableElement(); + assert.notIncludesClass( + node1.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + node2.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered tree focusTree()ed other tree node passively focused tree node now has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + this.focusManager.focusTree(this.testFocusableGroup1); + + // The original node in the tree should be moved from passive to active focus per the + // contract of focusTree). Also, the root of the tree should have no focus indication. + const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); + const rootElem = this.testFocusableGroup1 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('focus on root, node in diff tree, then node in first tree; root should have focus gone', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusTree(this.testFocusableGroup1); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); + const rootElem = this.testFocusableGroup1 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('nested tree focusTree()ed with no prev root has active focus', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + + this.focusManager.focusTree(this.testFocusableNestedGroup4); + + const rootElem = this.testFocusableNestedGroup4 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('nested tree node focusNode()ed with no prev focus node has active focus', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + + this.focusManager.focusNode(this.testFocusableNestedGroup4Node1); + + const nodeElem = + this.testFocusableNestedGroup4Node1.getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('nested tree node focusNode()ed after parent focused prev has passive node has active', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + this.focusManager.focusNode(this.testFocusableNestedGroup4Node1); + + const prevNodeElem = + this.testFocusableGroup2Node1.getFocusableElement(); + const currNodeElem = + this.testFocusableNestedGroup4Node1.getFocusableElement(); + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + currNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + }); + }); + + suite('DOM focus() switching in SVG tree', function () { + suite('getFocusedTree()', function () { + test('registered root focus()ed no prev focus returns tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1').tabIndex = -1; + + document.getElementById('testFocusableGroup1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup1, + ); + }); + + test("registered node focus()ed no prev focus returns node's tree", function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + + document.getElementById('testFocusableGroup1.node1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup1, + ); + }); + + test("registered subnode focus()ed no prev focus returns node's tree", function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1.child1').tabIndex = + -1; + + document.getElementById('testFocusableGroup1.node1.child1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup1, + ); + }); + + test('registered node focus()ed after prev node focus returns same tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + document.getElementById('testFocusableGroup1.node2').tabIndex = -1; + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testFocusableGroup1.node2').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup1, + ); + }); + + test("registered node focus()ed after prev node focus diff tree returns new node's tree", function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + document.getElementById('testFocusableGroup2.node1').tabIndex = -1; + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testFocusableGroup2.node1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + }); + + test("registered tree root focus()ed after prev node focus diff tree returns new node's tree", function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + document.getElementById('testFocusableGroup2').tabIndex = -1; + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testFocusableGroup2').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + }); + + test("non-registered node subelement focus()ed returns node's tree", function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById( + 'testFocusableGroup1.node2.unregisteredChild1', + ).tabIndex = -1; + + document + .getElementById('testFocusableGroup1.node2.unregisteredChild1') + .focus(); + + // The tree of the unregistered child element should take focus. + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup1, + ); + }); + + test('non-registered tree focus()ed returns null', function () { + document.getElementById('testUnregisteredFocusableGroup3').tabIndex = + -1; + + document.getElementById('testUnregisteredFocusableGroup3').focus(); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('non-registered tree node focus()ed returns null', function () { + document.getElementById( + 'testUnregisteredFocusableGroup3.node1', + ).tabIndex = -1; + + document + .getElementById('testUnregisteredFocusableGroup3.node1') + .focus(); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('non-registered tree node focus()ed after registered node focused returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + document.getElementById( + 'testUnregisteredFocusableGroup3.node1', + ).tabIndex = -1; + document.getElementById('testFocusableGroup1.node1').focus(); + + document + .getElementById('testUnregisteredFocusableGroup3.node1') + .focus(); + + // Attempting to focus a now removed tree should result in nothing being + // focused since the removed tree can have DOM focus, but that focus is + // ignored by FocusManager. + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focus()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1').tabIndex = -1; + document.getElementById('testFocusableGroup1').focus(); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focus()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + document.getElementById('testFocusableGroup1.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focus()ed with prev node prior focused returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup2.node1').tabIndex = -1; + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + document.getElementById('testFocusableGroup2.node1').focus(); + document.getElementById('testFocusableGroup1.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the more recent tree was removed, there's no tree currently focused. + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focus()ed with prev node recently focused returns new tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + document.getElementById('testFocusableGroup2.node1').tabIndex = -1; + document.getElementById('testFocusableGroup1.node1').focus(); + document.getElementById('testFocusableGroup2.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the most recent tree still exists, it still has focus. + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + }); + + test('unregistered tree focus()ed with prev node after unregistering returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + document.getElementById('testFocusableGroup2.node1').tabIndex = -1; + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + document.getElementById('testFocusableGroup1.node1').focus(); + document.getElementById('testFocusableGroup2.node1').focus(); + this.focusManager.unregisterTree(this.testFocusableGroup1); + + document.getElementById('testFocusableGroup1.node1').focus(); + + // Attempting to focus a now removed tree should result in nothing being + // focused since the removed tree can have DOM focus, but that focus is + // ignored by FocusManager. + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('nested tree focusTree()ed with no prev focus returns nested tree', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + document.getElementById('testFocusableNestedGroup4').tabIndex = -1; + + document.getElementById('testFocusableNestedGroup4').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableNestedGroup4, + ); + }); + + test('nested tree node focusNode()ed with no prev focus returns nested tree', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + document.getElementById('testFocusableNestedGroup4.node1').tabIndex = + -1; + + document.getElementById('testFocusableNestedGroup4.node1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableNestedGroup4, + ); + }); + + test('nested tree node focusNode()ed after parent focused returns nested tree', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + document.getElementById('testFocusableGroup2.node1').tabIndex = -1; + document.getElementById('testFocusableNestedGroup4.node1').tabIndex = + -1; + document.getElementById('testFocusableGroup2.node1').focus(); + + document.getElementById('testFocusableNestedGroup4.node1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableNestedGroup4, + ); + }); + }); + suite('getFocusedNode()', function () { + test('registered root focus()ed no prev focus returns root node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1').tabIndex = -1; + + document.getElementById('testFocusableGroup1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1.getRootFocusableNode(), + ); + }); + + test('registered node focus()ed no prev focus returns node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + + document.getElementById('testFocusableGroup1.node1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1Node1, + ); + }); + + test('registered subnode focus()ed no prev focus returns subnode', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1.child1').tabIndex = + -1; + + document.getElementById('testFocusableGroup1.node1.child1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1Node1Child1, + ); + }); + + test('registered node focus()ed after prev node focus returns new node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + document.getElementById('testFocusableGroup1.node2').tabIndex = -1; + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testFocusableGroup1.node2').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1Node2, + ); + }); + + test('registered node focus()ed after prev node focus diff tree returns new node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + document.getElementById('testFocusableGroup2.node1').tabIndex = -1; + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testFocusableGroup2.node1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + }); + + test('registered tree root focus()ed after prev node focus diff tree returns new root', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + document.getElementById('testFocusableGroup2').tabIndex = -1; + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testFocusableGroup2').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2.getRootFocusableNode(), + ); + }); + + test('non-registered node subelement focus()ed returns nearest node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById( + 'testFocusableGroup1.node2.unregisteredChild1', + ).tabIndex = -1; + + document + .getElementById('testFocusableGroup1.node2.unregisteredChild1') + .focus(); + + // The nearest node of the unregistered child element should take focus. + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1Node2, + ); + }); + + test('non-registered tree focus()ed returns null', function () { + document.getElementById('testUnregisteredFocusableGroup3').tabIndex = + -1; + + document.getElementById('testUnregisteredFocusableGroup3').focus(); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('non-registered tree node focus()ed returns null', function () { + document.getElementById( + 'testUnregisteredFocusableGroup3.node1', + ).tabIndex = -1; + + document + .getElementById('testUnregisteredFocusableGroup3.node1') + .focus(); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('non-registered tree node focus()ed after registered node focused returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + document.getElementById( + 'testUnregisteredFocusableGroup3.node1', + ).tabIndex = -1; + document.getElementById('testFocusableGroup1.node1').focus(); + + document + .getElementById('testUnregisteredFocusableGroup3.node1') + .focus(); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unfocusable element focus()ed after registered node focused returns original node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testUnfocusableElement').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1Node1, + ); + }); + + test('unregistered tree focus()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1').tabIndex = -1; + document.getElementById('testFocusableGroup1').focus(); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unregistered tree focus()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + document.getElementById('testFocusableGroup1.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unregistered tree focus()ed with prev node prior focused returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup2.node1').tabIndex = -1; + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + document.getElementById('testFocusableGroup2.node1').focus(); + document.getElementById('testFocusableGroup1.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the more recent tree was removed, there's no tree currently focused. + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unregistered tree focus()ed with prev node recently focused returns new node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + document.getElementById('testFocusableGroup2.node1').tabIndex = -1; + document.getElementById('testFocusableGroup1.node1').focus(); + document.getElementById('testFocusableGroup2.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the most recent tree still exists, it still has focus. + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + }); + + test('unregistered tree focus()ed with prev node after unregistering returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + document.getElementById('testFocusableGroup2.node1').tabIndex = -1; + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + document.getElementById('testFocusableGroup1.node1').focus(); + document.getElementById('testFocusableGroup2.node1').focus(); + this.focusManager.unregisterTree(this.testFocusableGroup1); + + document.getElementById('testFocusableGroup1.node1').focus(); + + // Attempting to focus a now removed tree should result in nothing being + // focused since the removed tree can have DOM focus, but that focus is + // ignored by FocusManager. + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('nested tree focus()ed with no prev focus returns nested root', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + document.getElementById('testFocusableNestedGroup4').tabIndex = -1; + + document.getElementById('testFocusableNestedGroup4').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableNestedGroup4.getRootFocusableNode(), + ); + }); + + test('nested tree node focus()ed with no prev focus returns focused node', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + document.getElementById('testFocusableNestedGroup4.node1').tabIndex = + -1; + + document.getElementById('testFocusableNestedGroup4.node1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableNestedGroup4Node1, + ); + }); + + test('nested tree node focus()ed after parent focused returns focused node', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + document.getElementById('testFocusableGroup2.node1').tabIndex = -1; + document.getElementById('testFocusableNestedGroup4.node1').tabIndex = + -1; + document.getElementById('testFocusableGroup2.node1').focus(); + + document.getElementById('testFocusableNestedGroup4.node1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableNestedGroup4Node1, + ); + }); + + test('after focus() after trying to focusNode() an unfocusable node updates returns focus()ed node', function () { + this.testFocusableGroup1Node1.canBeFocused = () => false; + document.getElementById('testFocusableGroup1.node2').tabIndex = -1; + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + document.getElementById('testFocusableGroup1.node2').focus(); + + // focus()ing a new node should overwrite a failed attempt to focusNode() an unfocusable + // node. This verifies that DOM focus syncing is properly reenabled by FocusManager. + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1Node2, + ); + }); + + test('after focus() after trying to focusNode() the same node twice returns focus()ed node', function () { + document.getElementById('testFocusableGroup1.node2').tabIndex = -1; + this.focusManager.registerTree(this.testFocusableGroup1); + // Intentionally try to focus the same node twice. + this.focusManager.focusNode(this.testFocusableGroup1Node1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + document.getElementById('testFocusableGroup1.node2').focus(); + + // focus()ing a new node should overwrite a failed attempt to focusNode() the same node + // twice. This verifies that DOM focus syncing is properly reenabled by FocusManager. + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1Node2, + ); + }); + }); + suite('CSS classes', function () { + test('registered root focus()ed no prev focus returns root elem has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1').tabIndex = -1; + + document.getElementById('testFocusableGroup1').focus(); + + const rootElem = this.testFocusableGroup1 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focus()ed no prev focus node elem has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + + document.getElementById('testFocusableGroup1.node1').focus(); + + const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focus()ed after prev node focus same tree old node elem has no focus property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + document.getElementById('testFocusableGroup1.node2').tabIndex = -1; + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testFocusableGroup1.node2').focus(); + + const prevNodeElem = + this.testFocusableGroup1Node1.getFocusableElement(); + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focus()ed after prev node focus same tree new node elem has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + document.getElementById('testFocusableGroup1.node2').tabIndex = -1; + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testFocusableGroup1.node2').focus(); + + const newNodeElem = this.testFocusableGroup1Node2.getFocusableElement(); + assert.includesClass( + newNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + newNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focus()ed after prev node focus diff tree old node elem has passive property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + document.getElementById('testFocusableGroup2.node1').tabIndex = -1; + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testFocusableGroup2.node1').focus(); + + const prevNodeElem = + this.testFocusableGroup1Node1.getFocusableElement(); + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focus()ed after prev node focus diff tree new node elem has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + document.getElementById('testFocusableGroup2.node1').tabIndex = -1; + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testFocusableGroup2.node1').focus(); + + const newNodeElem = this.testFocusableGroup2Node1.getFocusableElement(); + assert.includesClass( + newNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + newNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered tree root focus()ed after prev node focus diff tree new root has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + document.getElementById('testFocusableGroup2').tabIndex = -1; + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testFocusableGroup2').focus(); + + const rootElem = this.testFocusableGroup2 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('non-registered node subelement focus()ed nearest node has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById( + 'testFocusableGroup1.node2.unregisteredChild1', + ).tabIndex = -1; + + document + .getElementById('testFocusableGroup1.node2.unregisteredChild1') + .focus(); + + // The nearest node of the unregistered child element should be actively focused. + const nodeElem = this.testFocusableGroup1Node2.getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('non-registered tree focus()ed has no focus', function () { + document.getElementById('testUnregisteredFocusableGroup3').tabIndex = + -1; + + document.getElementById('testUnregisteredFocusableGroup3').focus(); + + assert.isNull(this.focusManager.getFocusedNode()); + const rootElem = document.getElementById( + 'testUnregisteredFocusableGroup3', + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('non-registered tree node focus()ed has no focus', function () { + document.getElementById( + 'testUnregisteredFocusableGroup3.node1', + ).tabIndex = -1; + + document + .getElementById('testUnregisteredFocusableGroup3.node1') + .focus(); + + assert.isNull(this.focusManager.getFocusedNode()); + const nodeElem = document.getElementById( + 'testUnregisteredFocusableGroup3.node1', + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unfocusable element focus()ed after registered node focused original node has active focus', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testUnfocusableElement').focus(); + + // The original node should be unchanged, and the unregistered node should not have any + // focus indicators. + const nodeElem = document.getElementById('testFocusableGroup1.node1'); + const attemptedNewNodeElem = document.getElementById( + 'testUnfocusableElement', + ); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + attemptedNewNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + attemptedNewNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unregistered tree focus()ed with no prev focus removes focus', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1').tabIndex = -1; + document.getElementById('testFocusableGroup1').focus(); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the tree was unregistered it no longer has focus indicators. + const rootElem = this.testFocusableGroup1 + .getRootFocusableNode() + .getFocusableElement(); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unregistered tree focus()ed with no prev focus removes focus', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + document.getElementById('testFocusableGroup1.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the tree was unregistered it no longer has focus indicators. + const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unregistered tree focus()ed with prev node prior removes focus from removed tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup2.node1').tabIndex = -1; + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + document.getElementById('testFocusableGroup2.node1').focus(); + document.getElementById('testFocusableGroup1.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the tree was unregistered it no longer has focus indicators. However, the old node + // should still have passive indication. + const otherNodeElem = + this.testFocusableGroup2Node1.getFocusableElement(); + const removedNodeElem = + this.testFocusableGroup1Node1.getFocusableElement(); + assert.includesClass( + otherNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + otherNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unregistered tree focus()ed with prev node recently removes focus from removed tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + document.getElementById('testFocusableGroup2.node1').tabIndex = -1; + document.getElementById('testFocusableGroup1.node1').focus(); + document.getElementById('testFocusableGroup2.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the tree was unregistered it no longer has focus indicators. However, the new node + // should still have active indication. + const otherNodeElem = + this.testFocusableGroup2Node1.getFocusableElement(); + const removedNodeElem = + this.testFocusableGroup1Node1.getFocusableElement(); + assert.includesClass( + otherNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + otherNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unregistered tree focus()ed with prev node after unregistering removes active indicator', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + document.getElementById('testFocusableGroup2.node1').tabIndex = -1; + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + document.getElementById('testFocusableGroup1.node1').focus(); + document.getElementById('testFocusableGroup2.node1').focus(); + this.focusManager.unregisterTree(this.testFocusableGroup1); + + document.getElementById('testFocusableGroup1.node1').focus(); + + // Attempting to focus a now removed tree should remove active. + const otherNodeElem = + this.testFocusableGroup2Node1.getFocusableElement(); + const removedNodeElem = + this.testFocusableGroup1Node1.getFocusableElement(); + assert.notIncludesClass( + otherNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + otherNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('focus() multiple nodes in same tree with switches ensure passive focus has gone', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + document.getElementById('testFocusableGroup2.node1').tabIndex = -1; + document.getElementById('testFocusableGroup1.node2').tabIndex = -1; + document.getElementById('testFocusableGroup1.node1').focus(); + document.getElementById('testFocusableGroup2.node1').focus(); + + document.getElementById('testFocusableGroup1.node2').focus(); + + // When switching back to the first tree, ensure the original passive node is no longer + // passive now that the new node is active. + const node1 = this.testFocusableGroup1Node1.getFocusableElement(); + const node2 = this.testFocusableGroup1Node2.getFocusableElement(); + assert.notIncludesClass( + node1.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + node2.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered tree focus()ed other tree node passively focused tree node now has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + document.getElementById('testFocusableGroup2.node1').tabIndex = -1; + document.getElementById('testFocusableGroup1').tabIndex = -1; + document.getElementById('testFocusableGroup1.node1').focus(); + document.getElementById('testFocusableGroup2.node1').focus(); + + document.getElementById('testFocusableGroup1').focus(); + + // Directly refocusing a tree's root should have functional parity with focusTree(). That + // means the tree's previous node should now have active focus again and its root should + // have no focus indication. + const rootElem = this.testFocusableGroup1 + .getRootFocusableNode() + .getFocusableElement(); + const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('focus on root, node in diff tree, then node in first tree; root should have focus gone', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1').tabIndex = -1; + document.getElementById('testFocusableGroup2.node1').tabIndex = -1; + document.getElementById('testFocusableGroup1.node1').tabIndex = -1; + document.getElementById('testFocusableGroup1').focus(); + document.getElementById('testFocusableGroup2.node1').focus(); + + document.getElementById('testFocusableGroup1.node1').focus(); + + const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); + const rootElem = this.testFocusableGroup1 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('nested tree focus()ed with no prev root has active focus', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + document.getElementById('testFocusableNestedGroup4').tabIndex = -1; + + document.getElementById('testFocusableNestedGroup4').focus(); + + const rootElem = this.testFocusableNestedGroup4 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('nested tree node focus()ed with no prev focus node has active focus', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + document.getElementById('testFocusableNestedGroup4.node1').tabIndex = + -1; + + document.getElementById('testFocusableNestedGroup4.node1').focus(); + + const nodeElem = + this.testFocusableNestedGroup4Node1.getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('nested tree node focus()ed after parent focused prev has passive node has active', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + document.getElementById('testFocusableGroup2.node1').tabIndex = -1; + document.getElementById('testFocusableNestedGroup4.node1').tabIndex = + -1; + document.getElementById('testFocusableGroup2.node1').focus(); + + document.getElementById('testFocusableNestedGroup4.node1').focus(); + + const prevNodeElem = + this.testFocusableGroup2Node1.getFocusableElement(); + const currNodeElem = + this.testFocusableNestedGroup4Node1.getFocusableElement(); + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + currNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + }); + }); + + /* High-level focus/defocusing tests. */ + suite('Defocusing and refocusing', function () { + test('Defocusing actively focused root HTML tree switches to passive highlight', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusTree(this.testFocusableTree2); + document.getElementById('testUnregisteredFocusableTree3').tabIndex = -1; + + document.getElementById('testUnregisteredFocusableTree3').focus(); + + const rootNode = this.testFocusableTree2.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + assert.isNull(this.focusManager.getFocusedTree()); + assert.isNull(this.focusManager.getFocusedNode()); + assert.includesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('Defocusing actively focused HTML tree node switches to passive highlight', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + document.getElementById('testUnregisteredFocusableTree3').tabIndex = -1; + + document.getElementById('testUnregisteredFocusableTree3').focus(); + + const nodeElem = this.testFocusableTree2Node1.getFocusableElement(); + assert.isNull(this.focusManager.getFocusedTree()); + assert.isNull(this.focusManager.getFocusedNode()); + assert.includesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('Defocusing actively focused HTML subtree node switches to passive highlight', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + this.focusManager.focusNode(this.testFocusableNestedTree4Node1); + document.getElementById('testUnregisteredFocusableTree3').tabIndex = -1; + + document.getElementById('testUnregisteredFocusableTree3').focus(); + + const nodeElem = this.testFocusableNestedTree4Node1.getFocusableElement(); + assert.isNull(this.focusManager.getFocusedTree()); + assert.isNull(this.focusManager.getFocusedNode()); + assert.includesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('Refocusing actively focused root HTML tree restores to active highlight', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusTree(this.testFocusableTree2); + document.getElementById('testUnregisteredFocusableTree3').tabIndex = -1; + document.getElementById('testFocusableTree2').tabIndex = -1; + document.getElementById('testUnregisteredFocusableTree3').focus(); + + document.getElementById('testFocusableTree2').focus(); + + const rootNode = this.testFocusableTree2.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + assert.strictEqual(this.focusManager.getFocusedNode(), rootNode); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('Refocusing actively focused HTML tree node restores to active highlight', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + document.getElementById('testUnregisteredFocusableTree3').tabIndex = -1; + document.getElementById('testFocusableTree2.node1').tabIndex = -1; + document.getElementById('testUnregisteredFocusableTree3').focus(); + + document.getElementById('testFocusableTree2.node1').focus(); + + const nodeElem = this.testFocusableTree2Node1.getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('Refocusing actively focused HTML subtree node restores to active highlight', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + this.focusManager.focusNode(this.testFocusableNestedTree4Node1); + document.getElementById('testUnregisteredFocusableTree3').tabIndex = -1; + document.getElementById('testFocusableNestedTree4.node1').tabIndex = -1; + document.getElementById('testUnregisteredFocusableTree3').focus(); + + document.getElementById('testFocusableNestedTree4.node1').focus(); + + const nodeElem = this.testFocusableNestedTree4Node1.getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableNestedTree4, + ); + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableNestedTree4Node1, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + }); + + /* Combined HTML/SVG tree focus tests. */ + + suite('HTML/SVG focus tree switching', function () { + suite('Focus HTML tree then SVG tree', function () { + test('HTML focusTree()ed then SVG focusTree()ed correctly updates getFocusedTree() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusTree(this.testFocusableTree2); + + this.focusManager.focusTree(this.testFocusableGroup2); + + const prevElem = this.testFocusableTree2 + .getRootFocusableNode() + .getFocusableElement(); + const currElem = this.testFocusableGroup2 + .getRootFocusableNode() + .getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + assert.strictEqual(document.activeElement, currElem); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('HTML focusTree()ed then SVG focusNode()ed correctly updates getFocusedNode() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusTree(this.testFocusableTree2); + + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + const prevElem = this.testFocusableTree2 + .getRootFocusableNode() + .getFocusableElement(); + const currElem = this.testFocusableGroup2Node1.getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + assert.strictEqual(document.activeElement, currElem); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('HTML focusTree()ed then SVG DOM focus()ed correctly updates getFocusedNode() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusTree(this.testFocusableTree2); + document.getElementById('testFocusableGroup2.node1').tabIndex = -1; + + document.getElementById('testFocusableGroup2.node1').focus(); + + const prevElem = this.testFocusableTree2 + .getRootFocusableNode() + .getFocusableElement(); + const currElem = this.testFocusableGroup2Node1.getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + assert.strictEqual(document.activeElement, currElem); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('HTML focusNode()ed then SVG focusTree()ed correctly updates getFocusedTree() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + this.focusManager.focusTree(this.testFocusableGroup2); + + const prevElem = this.testFocusableTree2Node1.getFocusableElement(); + const currElem = this.testFocusableGroup2 + .getRootFocusableNode() + .getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + assert.strictEqual(document.activeElement, currElem); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('HTML focusNode()ed then SVG focusNode()ed correctly updates getFocusedNode() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + const prevElem = this.testFocusableTree2Node1.getFocusableElement(); + const currElem = this.testFocusableGroup2Node1.getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + assert.strictEqual(document.activeElement, currElem); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('HTML focusNode()ed then SVG DOM focus()ed correctly updates getFocusedNode() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + document.getElementById('testFocusableGroup2.node1').tabIndex = -1; + + document.getElementById('testFocusableGroup2.node1').focus(); + + const prevElem = this.testFocusableTree2Node1.getFocusableElement(); + const currElem = this.testFocusableGroup2Node1.getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + assert.strictEqual(document.activeElement, currElem); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('HTML DOM focus()ed then SVG focusTree()ed correctly updates getFocusedTree() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableTree2.node1').tabIndex = -1; + document.getElementById('testFocusableTree2.node1').focus(); + + this.focusManager.focusTree(this.testFocusableGroup2); + + const prevElem = this.testFocusableTree2Node1.getFocusableElement(); + const currElem = this.testFocusableGroup2 + .getRootFocusableNode() + .getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + assert.strictEqual(document.activeElement, currElem); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('HTML DOM focus()ed then SVG focusNode()ed correctly updates getFocusedNode() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableTree2.node1').tabIndex = -1; + document.getElementById('testFocusableTree2.node1').focus(); + + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + const prevElem = this.testFocusableTree2Node1.getFocusableElement(); + const currElem = this.testFocusableGroup2Node1.getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + assert.strictEqual(document.activeElement, currElem); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('HTML DOM focus()ed then SVG DOM focus()ed correctly updates getFocusedNode() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableTree2.node1').tabIndex = -1; + document.getElementById('testFocusableGroup2.node1').tabIndex = -1; + document.getElementById('testFocusableTree2.node1').focus(); + + document.getElementById('testFocusableGroup2.node1').focus(); + + const prevElem = this.testFocusableTree2Node1.getFocusableElement(); + const currElem = this.testFocusableGroup2Node1.getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + assert.strictEqual(document.activeElement, currElem); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + }); + suite('Focus SVG tree then HTML tree', function () { + test('SVG focusTree()ed then HTML focusTree()ed correctly updates getFocusedTree() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusTree(this.testFocusableGroup2); + + this.focusManager.focusTree(this.testFocusableTree2); + + const prevElem = this.testFocusableGroup2 + .getRootFocusableNode() + .getFocusableElement(); + const currElem = this.testFocusableTree2 + .getRootFocusableNode() + .getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + assert.strictEqual(document.activeElement, currElem); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('SVG focusTree()ed then HTML focusNode()ed correctly updates getFocusedNode() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusTree(this.testFocusableGroup2); + + this.focusManager.focusNode(this.testFocusableTree2Node1); + + const prevElem = this.testFocusableGroup2 + .getRootFocusableNode() + .getFocusableElement(); + const currElem = this.testFocusableTree2Node1.getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + assert.strictEqual(document.activeElement, currElem); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('SVG focusTree()ed then HTML DOM focus()ed correctly updates getFocusedNode() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusTree(this.testFocusableGroup2); + document.getElementById('testFocusableTree2.node1').tabIndex = -1; + + document.getElementById('testFocusableTree2.node1').focus(); + + const prevElem = this.testFocusableGroup2 + .getRootFocusableNode() + .getFocusableElement(); + const currElem = this.testFocusableTree2Node1.getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + assert.strictEqual(document.activeElement, currElem); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('SVG focusNode()ed then HTML focusTree()ed correctly updates getFocusedTree() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + this.focusManager.focusTree(this.testFocusableTree2); + + const prevElem = this.testFocusableGroup2Node1.getFocusableElement(); + const currElem = this.testFocusableTree2 + .getRootFocusableNode() + .getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + assert.strictEqual(document.activeElement, currElem); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('SVG focusNode()ed then HTML focusNode()ed correctly updates getFocusedNode() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + this.focusManager.focusNode(this.testFocusableTree2Node1); + + const prevElem = this.testFocusableGroup2Node1.getFocusableElement(); + const currElem = this.testFocusableTree2Node1.getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + assert.strictEqual(document.activeElement, currElem); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('SVG focusNode()ed then HTML DOM focus()ed correctly updates getFocusedNode() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + document.getElementById('testFocusableTree2.node1').tabIndex = -1; + + document.getElementById('testFocusableTree2.node1').focus(); + + const prevElem = this.testFocusableGroup2Node1.getFocusableElement(); + const currElem = this.testFocusableTree2Node1.getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + assert.strictEqual(document.activeElement, currElem); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('SVG DOM focus()ed then HTML focusTree()ed correctly updates getFocusedTree() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup2.node1').tabIndex = -1; + document.getElementById('testFocusableGroup2.node1').focus(); + + this.focusManager.focusTree(this.testFocusableTree2); + + const prevElem = this.testFocusableGroup2Node1.getFocusableElement(); + const currElem = this.testFocusableTree2 + .getRootFocusableNode() + .getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + assert.strictEqual(document.activeElement, currElem); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('SVG DOM focus()ed then HTML focusNode()ed correctly updates getFocusedNode() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup2.node1').tabIndex = -1; + document.getElementById('testFocusableGroup2.node1').focus(); + + this.focusManager.focusNode(this.testFocusableTree2Node1); + + const prevElem = this.testFocusableGroup2Node1.getFocusableElement(); + const currElem = this.testFocusableTree2Node1.getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + assert.strictEqual(document.activeElement, currElem); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('SVG DOM focus()ed then HTML DOM focus()ed correctly updates getFocusedNode() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup2.node1').tabIndex = -1; + document.getElementById('testFocusableTree2.node1').tabIndex = -1; + document.getElementById('testFocusableGroup2.node1').focus(); + + document.getElementById('testFocusableTree2.node1').focus(); + + const prevElem = this.testFocusableGroup2Node1.getFocusableElement(); + const currElem = this.testFocusableTree2Node1.getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + assert.strictEqual(document.activeElement, currElem); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + }); + }); + + /* Ephemeral focus tests. */ + + suite('takeEphemeralFocus()', function () { + setup(function () { + // Ensure ephemeral-specific elements are focusable. + document.getElementById('nonTreeElementForEphemeralFocus').tabIndex = -1; + document.getElementById('nonTreeGroupForEphemeralFocus').tabIndex = -1; + }); + teardown(function () { + // Ensure ephemeral-specific elements have their tab indexes reset for a clean state. + document + .getElementById('nonTreeElementForEphemeralFocus') + .removeAttribute('tabindex'); + document + .getElementById('nonTreeGroupForEphemeralFocus') + .removeAttribute('tabindex'); + }); + + test('with no focused node does not change states', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + + const ephemeralElement = document.getElementById( + 'nonTreeElementForEphemeralFocus', + ); + this.focusManager.takeEphemeralFocus(ephemeralElement); + + // Taking focus without an existing node having focus should change no focus indicators. + const activeElems = Array.from( + document.querySelectorAll(ACTIVE_FOCUS_NODE_CSS_SELECTOR), + ); + const passiveElems = Array.from( + document.querySelectorAll(PASSIVE_FOCUS_NODE_CSS_SELECTOR), + ); + assert.isEmpty(activeElems); + assert.isEmpty(passiveElems); + }); + + test('with focused node changes focused node to passive', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + const ephemeralElement = document.getElementById( + 'nonTreeElementForEphemeralFocus', + ); + this.focusManager.takeEphemeralFocus(ephemeralElement); + + // Taking focus without an existing node having focus should change no focus indicators. + const activeElems = Array.from( + document.querySelectorAll(ACTIVE_FOCUS_NODE_CSS_SELECTOR), + ); + const passiveElems = Array.from( + document.querySelectorAll(PASSIVE_FOCUS_NODE_CSS_SELECTOR), + ); + assert.isEmpty(activeElems); + assert.strictEqual(passiveElems.length, 1); + assert.includesClass( + this.testFocusableTree2Node1.getFocusableElement().classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('focuses provided HTML element', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + + const ephemeralElement = document.getElementById( + 'nonTreeElementForEphemeralFocus', + ); + this.focusManager.takeEphemeralFocus(ephemeralElement); + + assert.strictEqual(document.activeElement, ephemeralElement); + }); + + test('focuses provided SVG element', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + + const ephemeralElement = document.getElementById( + 'nonTreeGroupForEphemeralFocus', + ); + this.focusManager.takeEphemeralFocus(ephemeralElement); + + assert.strictEqual(document.activeElement, ephemeralElement); + }); + + test('twice for without finishing previous throws error', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + const ephemeralGroupElement = document.getElementById( + 'nonTreeGroupForEphemeralFocus', + ); + const ephemeralDivElement = document.getElementById( + 'nonTreeElementForEphemeralFocus', + ); + this.focusManager.takeEphemeralFocus(ephemeralGroupElement); + + const errorMsgRegex = + /Attempted to take ephemeral focus when it's already held+?/; + assert.throws( + () => this.focusManager.takeEphemeralFocus(ephemeralDivElement), + errorMsgRegex, + ); + }); + + test('then focusTree() changes getFocusedTree() but not active state', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusTree(this.testFocusableTree2); + const ephemeralElement = document.getElementById( + 'nonTreeGroupForEphemeralFocus', + ); + this.focusManager.takeEphemeralFocus(ephemeralElement); + + this.focusManager.focusTree(this.testFocusableGroup2); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + const activeElems = Array.from( + document.querySelectorAll(ACTIVE_FOCUS_NODE_CSS_SELECTOR), + ); + assert.isEmpty(activeElems); + }); + + test('then focusNode() changes getFocusedNode() but not active state', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + const ephemeralElement = document.getElementById( + 'nonTreeGroupForEphemeralFocus', + ); + this.focusManager.takeEphemeralFocus(ephemeralElement); + + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + const activeElems = Array.from( + document.querySelectorAll(ACTIVE_FOCUS_NODE_CSS_SELECTOR), + ); + assert.isEmpty(activeElems); + }); + + test('then DOM refocus changes getFocusedNode() but not active state', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + document.getElementById('testFocusableGroup2.node1').tabIndex = -1; + const ephemeralElement = document.getElementById( + 'nonTreeGroupForEphemeralFocus', + ); + this.focusManager.takeEphemeralFocus(ephemeralElement); + + // Force focus to change via the DOM. + document.getElementById('testFocusableGroup2.node1').focus(); + + // The focus() state change will affect getFocusedNode() but it will not cause the node to now + // be active. + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + const activeElems = Array.from( + document.querySelectorAll(ACTIVE_FOCUS_NODE_CSS_SELECTOR), + ); + assert.isEmpty(activeElems); + }); + + test('then finish ephemeral callback with no node does not change indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + const ephemeralElement = document.getElementById( + 'nonTreeElementForEphemeralFocus', + ); + const finishFocusCallback = + this.focusManager.takeEphemeralFocus(ephemeralElement); + + finishFocusCallback(); + + // Finishing ephemeral focus without a previously focused node should not change indicators. + const activeElems = Array.from( + document.querySelectorAll(ACTIVE_FOCUS_NODE_CSS_SELECTOR), + ); + const passiveElems = Array.from( + document.querySelectorAll(PASSIVE_FOCUS_NODE_CSS_SELECTOR), + ); + assert.isEmpty(activeElems); + assert.isEmpty(passiveElems); + }); + + test('again after finishing previous empheral focus should focus new element', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + const ephemeralGroupElement = document.getElementById( + 'nonTreeGroupForEphemeralFocus', + ); + const ephemeralDivElement = document.getElementById( + 'nonTreeElementForEphemeralFocus', + ); + const finishFocusCallback = this.focusManager.takeEphemeralFocus( + ephemeralGroupElement, + ); + + finishFocusCallback(); + this.focusManager.takeEphemeralFocus(ephemeralDivElement); + + // An exception should not be thrown and the new element should receive focus. + assert.strictEqual(document.activeElement, ephemeralDivElement); + }); + + test('calling ephemeral callback twice throws error', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + const ephemeralElement = document.getElementById( + 'nonTreeElementForEphemeralFocus', + ); + const finishFocusCallback = + this.focusManager.takeEphemeralFocus(ephemeralElement); + finishFocusCallback(); + + const errorMsgRegex = + /Attempted to finish ephemeral focus twice for element+?/; + assert.throws(() => finishFocusCallback(), errorMsgRegex); + }); + + test('then finish ephemeral callback should restore focused node', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + const ephemeralElement = document.getElementById( + 'nonTreeGroupForEphemeralFocus', + ); + const finishFocusCallback = + this.focusManager.takeEphemeralFocus(ephemeralElement); + + finishFocusCallback(); + + // The original focused node should be restored. + const nodeElem = this.testFocusableTree2Node1.getFocusableElement(); + const activeElems = Array.from( + document.querySelectorAll(ACTIVE_FOCUS_NODE_CSS_SELECTOR), + ); + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + assert.strictEqual(activeElems.length, 1); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.strictEqual(document.activeElement, nodeElem); + }); + + test('then focusTree() and finish ephemeral callback correctly sets new active state', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusTree(this.testFocusableTree2); + const ephemeralElement = document.getElementById( + 'nonTreeGroupForEphemeralFocus', + ); + const finishFocusCallback = + this.focusManager.takeEphemeralFocus(ephemeralElement); + + this.focusManager.focusTree(this.testFocusableGroup2); + finishFocusCallback(); + + // The tree's root should now be the active element since focus changed between the start and + // end of the ephemeral flow. + const rootElem = this.testFocusableGroup2 + .getRootFocusableNode() + .getFocusableElement(); + const activeElems = Array.from( + document.querySelectorAll(ACTIVE_FOCUS_NODE_CSS_SELECTOR), + ); + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + assert.strictEqual(activeElems.length, 1); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.strictEqual(document.activeElement, rootElem); + }); + + test('then focusNode() and finish ephemeral callback correctly sets new active state', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + const ephemeralElement = document.getElementById( + 'nonTreeGroupForEphemeralFocus', + ); + const finishFocusCallback = + this.focusManager.takeEphemeralFocus(ephemeralElement); + + this.focusManager.focusNode(this.testFocusableGroup2Node1); + finishFocusCallback(); + + // The tree's root should now be the active element since focus changed between the start and + // end of the ephemeral flow. + const nodeElem = this.testFocusableGroup2Node1.getFocusableElement(); + const activeElems = Array.from( + document.querySelectorAll(ACTIVE_FOCUS_NODE_CSS_SELECTOR), + ); + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + assert.strictEqual(activeElems.length, 1); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.strictEqual(document.activeElement, nodeElem); + }); + + test('then DOM focus change and finish ephemeral callback correctly sets new active state', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + document.getElementById('testFocusableGroup2.node1').tabIndex = -1; + const ephemeralElement = document.getElementById( + 'nonTreeGroupForEphemeralFocus', + ); + const finishFocusCallback = + this.focusManager.takeEphemeralFocus(ephemeralElement); + + document.getElementById('testFocusableGroup2.node1').focus(); + finishFocusCallback(); + + // The tree's root should now be the active element since focus changed between the start and + // end of the ephemeral flow. + const nodeElem = this.testFocusableGroup2Node1.getFocusableElement(); + const activeElems = Array.from( + document.querySelectorAll(ACTIVE_FOCUS_NODE_CSS_SELECTOR), + ); + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + assert.strictEqual(activeElems.length, 1); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.strictEqual(document.activeElement, nodeElem); + }); + }); +}); diff --git a/packages/blockly/tests/mocha/focusable_tree_traverser_test.js b/packages/blockly/tests/mocha/focusable_tree_traverser_test.js new file mode 100644 index 00000000000..a384dd4be45 --- /dev/null +++ b/packages/blockly/tests/mocha/focusable_tree_traverser_test.js @@ -0,0 +1,602 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {FocusManager} from '../../build/src/core/focus_manager.js'; +import {FocusableTreeTraverser} from '../../build/src/core/utils/focusable_tree_traverser.js'; +import {assert} from '../../node_modules/chai/index.js'; +import { + sharedTestSetup, + sharedTestTeardown, +} from './test_helpers/setup_teardown.js'; + +class FocusableNodeImpl { + constructor(element, tree) { + this.element = element; + this.tree = tree; + } + + getFocusableElement() { + return this.element; + } + + getFocusableTree() { + return this.tree; + } + + onNodeFocus() {} + + onNodeBlur() {} + + canBeFocused() { + return true; + } +} + +class FocusableTreeImpl { + constructor(rootElement, nestedTrees) { + this.nestedTrees = nestedTrees; + this.idToNodeMap = {}; + this.rootNode = this.addNode(rootElement); + } + + addNode(element) { + const node = new FocusableNodeImpl(element, this); + this.idToNodeMap[element.id] = node; + return node; + } + + getRootFocusableNode() { + return this.rootNode; + } + + getRestoredFocusableNode() { + return null; + } + + getNestedTrees() { + return this.nestedTrees; + } + + lookUpFocusableNode(id) { + return this.idToNodeMap[id]; + } + + onTreeFocus() {} + + onTreeBlur() {} +} + +suite('FocusableTreeTraverser', function () { + setup(function () { + sharedTestSetup.call(this); + + const createFocusableTree = function (rootElementId, nestedTrees) { + return new FocusableTreeImpl( + document.getElementById(rootElementId), + nestedTrees || [], + ); + }; + const createFocusableNode = function (tree, elementId) { + return tree.addNode(document.getElementById(elementId)); + }; + + this.testFocusableTree1 = createFocusableTree('testFocusableTree1'); + this.testFocusableTree1Node1 = createFocusableNode( + this.testFocusableTree1, + 'testFocusableTree1.node1', + ); + this.testFocusableTree1Node1Child1 = createFocusableNode( + this.testFocusableTree1, + 'testFocusableTree1.node1.child1', + ); + this.testFocusableTree1Node2 = createFocusableNode( + this.testFocusableTree1, + 'testFocusableTree1.node2', + ); + this.testFocusableNestedTree4 = createFocusableTree( + 'testFocusableNestedTree4', + ); + this.testFocusableNestedTree4Node1 = createFocusableNode( + this.testFocusableNestedTree4, + 'testFocusableNestedTree4.node1', + ); + this.testFocusableNestedTree5 = createFocusableTree( + 'testFocusableNestedTree5', + ); + this.testFocusableNestedTree5Node1 = createFocusableNode( + this.testFocusableNestedTree5, + 'testFocusableNestedTree5.node1', + ); + this.testFocusableTree2 = createFocusableTree('testFocusableTree2', [ + this.testFocusableNestedTree4, + this.testFocusableNestedTree5, + ]); + this.testFocusableTree2Node1 = createFocusableNode( + this.testFocusableTree2, + 'testFocusableTree2.node1', + ); + }); + + teardown(function () { + sharedTestTeardown.call(this); + + const removeFocusIndicators = function (element) { + element.classList.remove( + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }; + + // Ensure all node CSS styles are reset so that state isn't leaked between tests. + removeFocusIndicators(document.getElementById('testFocusableTree1')); + removeFocusIndicators(document.getElementById('testFocusableTree1.node1')); + removeFocusIndicators( + document.getElementById('testFocusableTree1.node1.child1'), + ); + removeFocusIndicators(document.getElementById('testFocusableTree1.node2')); + removeFocusIndicators(document.getElementById('testFocusableTree2')); + removeFocusIndicators(document.getElementById('testFocusableTree2.node1')); + removeFocusIndicators(document.getElementById('testFocusableNestedTree4')); + removeFocusIndicators( + document.getElementById('testFocusableNestedTree4.node1'), + ); + removeFocusIndicators(document.getElementById('testFocusableNestedTree5')); + removeFocusIndicators( + document.getElementById('testFocusableNestedTree5.node1'), + ); + }); + + suite('findFocusedNode()', function () { + test('for tree with no highlights returns null', function () { + const tree = this.testFocusableTree1; + + const finding = FocusableTreeTraverser.findFocusedNode(tree); + + assert.isNull(finding); + }); + + test('for tree with root active highlight returns root node', function () { + const tree = this.testFocusableTree1; + const rootNode = tree.getRootFocusableNode(); + rootNode + .getFocusableElement() + .classList.add(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + + const finding = FocusableTreeTraverser.findFocusedNode(tree); + + assert.strictEqual(finding, rootNode); + }); + + test('for tree with root passive highlight returns root node', function () { + const tree = this.testFocusableTree1; + const rootNode = tree.getRootFocusableNode(); + rootNode + .getFocusableElement() + .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + + const finding = FocusableTreeTraverser.findFocusedNode(tree); + + assert.strictEqual(finding, rootNode); + }); + + test('for tree with node active highlight returns node', function () { + const tree = this.testFocusableTree1; + const node = this.testFocusableTree1Node1; + node + .getFocusableElement() + .classList.add(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + + const finding = FocusableTreeTraverser.findFocusedNode(tree); + + assert.strictEqual(finding, node); + }); + + test('for tree with node passive highlight returns node', function () { + const tree = this.testFocusableTree1; + const node = this.testFocusableTree1Node1; + node + .getFocusableElement() + .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + + const finding = FocusableTreeTraverser.findFocusedNode(tree); + + assert.strictEqual(finding, node); + }); + + test('for tree with nested node active highlight returns node', function () { + const tree = this.testFocusableTree1; + const node = this.testFocusableTree1Node1Child1; + node + .getFocusableElement() + .classList.add(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + + const finding = FocusableTreeTraverser.findFocusedNode(tree); + + assert.strictEqual(finding, node); + }); + + test('for tree with nested node passive highlight returns node', function () { + const tree = this.testFocusableTree1; + const node = this.testFocusableTree1Node1Child1; + node + .getFocusableElement() + .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + + const finding = FocusableTreeTraverser.findFocusedNode(tree); + + assert.strictEqual(finding, node); + }); + + test('for tree with nested tree root active no parent highlights returns root', function () { + const tree = this.testFocusableNestedTree4; + const rootNode = this.testFocusableNestedTree4.getRootFocusableNode(); + rootNode + .getFocusableElement() + .classList.add(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + + const finding = FocusableTreeTraverser.findFocusedNode(tree); + + assert.strictEqual(finding, rootNode); + }); + + test('for tree with nested tree root passive no parent highlights returns root', function () { + const tree = this.testFocusableNestedTree4; + const rootNode = this.testFocusableNestedTree4.getRootFocusableNode(); + rootNode + .getFocusableElement() + .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + + const finding = FocusableTreeTraverser.findFocusedNode(tree); + + assert.strictEqual(finding, rootNode); + }); + + test('for tree with nested tree node active no parent highlights returns node', function () { + const tree = this.testFocusableNestedTree4; + const node = this.testFocusableNestedTree4Node1; + node + .getFocusableElement() + .classList.add(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + + const finding = FocusableTreeTraverser.findFocusedNode(tree); + + assert.strictEqual(finding, node); + }); + + test('for tree with nested tree root passive no parent highlights returns null', function () { + const tree = this.testFocusableNestedTree4; + const node = this.testFocusableNestedTree4Node1; + node + .getFocusableElement() + .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + + const finding = FocusableTreeTraverser.findFocusedNode(tree); + + assert.strictEqual(finding, node); + }); + + test('for tree with nested tree root active parent node passive returns parent node', function () { + const tree = this.testFocusableNestedTree4; + const rootNode = this.testFocusableNestedTree4.getRootFocusableNode(); + this.testFocusableTree2Node1 + .getFocusableElement() + .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + rootNode + .getFocusableElement() + .classList.add(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + + const finding = FocusableTreeTraverser.findFocusedNode( + this.testFocusableTree2, + ); + + assert.strictEqual(finding, this.testFocusableTree2Node1); + }); + + test('for tree with nested tree root passive parent node passive returns parent node', function () { + const tree = this.testFocusableNestedTree4; + const rootNode = this.testFocusableNestedTree4.getRootFocusableNode(); + this.testFocusableTree2Node1 + .getFocusableElement() + .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + rootNode + .getFocusableElement() + .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + + const finding = FocusableTreeTraverser.findFocusedNode( + this.testFocusableTree2, + ); + + assert.strictEqual(finding, this.testFocusableTree2Node1); + }); + + test('for tree with nested tree node active parent node passive returns parent node', function () { + const tree = this.testFocusableNestedTree4; + const node = this.testFocusableNestedTree4Node1; + this.testFocusableTree2Node1 + .getFocusableElement() + .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + node + .getFocusableElement() + .classList.add(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + + const finding = FocusableTreeTraverser.findFocusedNode( + this.testFocusableTree2, + ); + + assert.strictEqual(finding, this.testFocusableTree2Node1); + }); + + test('for tree with nested tree node passive parent node passive returns parent node', function () { + const tree = this.testFocusableNestedTree4; + const node = this.testFocusableNestedTree4Node1; + this.testFocusableTree2Node1 + .getFocusableElement() + .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + node + .getFocusableElement() + .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + + const finding = FocusableTreeTraverser.findFocusedNode( + this.testFocusableTree2, + ); + + assert.strictEqual(finding, this.testFocusableTree2Node1); + }); + }); + + suite('findFocusableNodeFor()', function () { + test('for element without ID returns null', function () { + const tree = this.testFocusableTree1; + const rootNode = tree.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + const oldId = rootElem.id; + // Normally it's not valid to miss an ID, but it can realistically happen. + rootElem.removeAttribute('id'); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + rootElem, + tree, + ); + // Restore the ID for other tests. + rootElem.setAttribute('id', oldId); + + assert.isNull(finding); + }); + + test('for element with null ID returns null', function () { + const tree = this.testFocusableTree1; + const rootNode = tree.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + const oldId = rootElem.id; + // Normally it's not valid to miss an ID, but it can realistically happen. + rootElem.setAttribute('id', null); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + rootElem, + tree, + ); + // Restore the ID for other tests. + rootElem.setAttribute('id', oldId); + + assert.isNull(finding); + }); + + test('for element with null ID string returns null', function () { + const tree = this.testFocusableTree1; + const rootNode = tree.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + const oldId = rootElem.id; + // This is a quirky version of the null variety above that's actually + // functionallity equivalent (since 'null' is converted to a string). + rootElem.setAttribute('id', 'null'); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + rootElem, + tree, + ); + // Restore the ID for other tests. + rootElem.setAttribute('id', oldId); + + assert.isNull(finding); + }); + + test('for element with empty ID returns null', function () { + const tree = this.testFocusableTree1; + const rootNode = tree.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + const oldId = rootElem.id; + // An empty ID is invalid since it will potentially conflict with other + // elements, and element IDs must be unique for focus management. + rootElem.setAttribute('id', ''); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + rootElem, + tree, + ); + // Restore the ID for other tests. + rootElem.setAttribute('id', oldId); + + assert.isNull(finding); + }); + + test('for root element returns root', function () { + const tree = this.testFocusableTree1; + const rootNode = tree.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + rootElem, + tree, + ); + + assert.strictEqual(finding, rootNode); + }); + + test('for element for different tree root returns null', function () { + const tree = this.testFocusableTree1; + const rootNode = this.testFocusableTree2.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + rootElem, + tree, + ); + + assert.isNull(finding); + }); + + test('for element for different tree node returns null', function () { + const tree = this.testFocusableTree1; + const nodeElem = this.testFocusableTree2Node1.getFocusableElement(); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + nodeElem, + tree, + ); + + assert.isNull(finding); + }); + + test('for node element in tree returns node', function () { + const tree = this.testFocusableTree1; + const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + nodeElem, + tree, + ); + + assert.strictEqual(finding, this.testFocusableTree1Node1); + }); + + test('for non-node element in tree returns root', function () { + const tree = this.testFocusableTree1; + const unregElem = document.getElementById( + 'testFocusableTree1.node2.unregisteredChild1', + ); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + unregElem, + tree, + ); + + // An unregistered element should map to the closest node. + assert.strictEqual(finding, this.testFocusableTree1Node2); + }); + + test('for nested node element in tree returns node', function () { + const tree = this.testFocusableTree1; + const nodeElem = this.testFocusableTree1Node1Child1.getFocusableElement(); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + nodeElem, + tree, + ); + + // The nested node should be returned. + assert.strictEqual(finding, this.testFocusableTree1Node1Child1); + }); + + test('for nested node element in tree returns node', function () { + const tree = this.testFocusableTree1; + const unregElem = document.getElementById( + 'testFocusableTree1.node1.child1.unregisteredChild1', + ); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + unregElem, + tree, + ); + + // An unregistered element should map to the closest node. + assert.strictEqual(finding, this.testFocusableTree1Node1Child1); + }); + + test('for nested node element in tree returns node', function () { + const tree = this.testFocusableTree1; + const unregElem = document.getElementById( + 'testFocusableTree1.unregisteredChild1', + ); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + unregElem, + tree, + ); + + // An unregistered element should map to the closest node (or root). + assert.strictEqual(finding, tree.getRootFocusableNode()); + }); + + test('for nested tree root returns nested tree root', function () { + const tree = this.testFocusableNestedTree4; + const rootNode = tree.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + rootElem, + tree, + ); + + assert.strictEqual(finding, rootNode); + }); + + test('for nested tree node returns nested tree node', function () { + const tree = this.testFocusableNestedTree4; + const nodeElem = this.testFocusableNestedTree4Node1.getFocusableElement(); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + nodeElem, + tree, + ); + + // The node of the nested tree should be returned. + assert.strictEqual(finding, this.testFocusableNestedTree4Node1); + }); + + test('for nested element in nested tree node returns nearest nested node', function () { + const tree = this.testFocusableNestedTree4; + const unregElem = document.getElementById( + 'testFocusableNestedTree4.node1.unregisteredChild1', + ); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + unregElem, + tree, + ); + + // An unregistered element should map to the closest node. + assert.strictEqual(finding, this.testFocusableNestedTree4Node1); + }); + + test('for nested tree node under root with different tree base returns null', function () { + const tree = this.testFocusableTree2; + const nodeElem = this.testFocusableNestedTree5Node1.getFocusableElement(); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + nodeElem, + tree, + ); + + // The nested node hierarchically sits below the outer tree, but using + // that tree as the basis should yield null since it's not a direct child. + assert.isNull(finding); + }); + + test('for nested tree node under node with different tree base returns null', function () { + const tree = this.testFocusableTree2; + const nodeElem = this.testFocusableNestedTree4Node1.getFocusableElement(); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + nodeElem, + tree, + ); + + // The nested node hierarchically sits below the outer tree, but using + // that tree as the basis should yield null since it's not a direct child. + assert.isNull(finding); + }); + }); +}); diff --git a/tests/mocha/generator_test.js b/packages/blockly/tests/mocha/generator_test.js similarity index 99% rename from tests/mocha/generator_test.js rename to packages/blockly/tests/mocha/generator_test.js index 527448eacc9..3c377e7c1ab 100644 --- a/tests/mocha/generator_test.js +++ b/packages/blockly/tests/mocha/generator_test.js @@ -10,7 +10,7 @@ import {JavascriptGenerator} from '../../build/src/generators/javascript/javascr import {LuaGenerator} from '../../build/src/generators/lua/lua_generator.js'; import {PhpGenerator} from '../../build/src/generators/php/php_generator.js'; import {PythonGenerator} from '../../build/src/generators/python/python_generator.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/gesture_test.js b/packages/blockly/tests/mocha/gesture_test.js similarity index 68% rename from tests/mocha/gesture_test.js rename to packages/blockly/tests/mocha/gesture_test.js index 3f53b8894b9..9036141ef25 100644 --- a/tests/mocha/gesture_test.js +++ b/packages/blockly/tests/mocha/gesture_test.js @@ -5,13 +5,14 @@ */ import {EventType} from '../../build/src/core/events/type.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {defineBasicBlockWithField} from './test_helpers/block_definitions.js'; import {assertEventFired, assertEventNotFired} from './test_helpers/events.js'; import { sharedTestSetup, sharedTestTeardown, } from './test_helpers/setup_teardown.js'; +import {getProperSimpleJson} from './test_helpers/toolbox_definitions.js'; import {dispatchPointerEvent} from './test_helpers/user_input.js'; suite('Gesture', function () { @@ -54,8 +55,12 @@ suite('Gesture', function () { setup(function () { sharedTestSetup.call(this); defineBasicBlockWithField(); - const toolbox = document.getElementById('gesture-test-toolbox'); - this.workspace = Blockly.inject('blocklyDiv', {toolbox: toolbox}); + const toolbox = getProperSimpleJson(); + toolbox.contents.unshift({ + 'kind': 'block', + 'type': 'test_field_block', + }); + this.workspace = Blockly.inject('blocklyDiv', {toolbox}); }); teardown(function () { @@ -94,4 +99,35 @@ suite('Gesture', function () { const block = getTopFlyoutBlock(flyout); testGestureIsFieldClick(block, true, this.eventsFireStub); }); + + test('Clicking on shadow block does not select it', function () { + const flyout = this.workspace.getFlyout(true); + flyout.createBlock( + flyout.getWorkspace().getBlocksByType('logic_compare')[0], + ); + const block = this.workspace.getBlocksByType('logic_compare')[0]; + const shadowBlock = block.getInput('A').connection.targetBlock(); + + this.eventsFireStub.resetHistory(); + const eventTarget = shadowBlock.getSvgRoot(); + dispatchPointerEvent(eventTarget, 'pointerdown'); + dispatchPointerEvent(eventTarget, 'pointerup'); + dispatchPointerEvent(eventTarget, 'click'); + + // The shadow block should not be selected, even though it was clicked. + assertEventNotFired( + this.eventsFireStub, + Blockly.Events.Selected, + {newElementId: shadowBlock.id, type: EventType.SELECTED}, + this.workspace.id, + ); + + // Its parent block should be selected, however. + assertEventFired( + this.eventsFireStub, + Blockly.Events.Selected, + {newElementId: block.id, type: EventType.SELECTED}, + this.workspace.id, + ); + }); }); diff --git a/tests/mocha/icon_test.js b/packages/blockly/tests/mocha/icon_test.js similarity index 85% rename from tests/mocha/icon_test.js rename to packages/blockly/tests/mocha/icon_test.js index 5855fcfc576..35117e3049b 100644 --- a/tests/mocha/icon_test.js +++ b/packages/blockly/tests/mocha/icon_test.js @@ -4,13 +4,34 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import * as Blockly from '../../build/src/core/blockly.js'; +import {assert} from '../../node_modules/chai/index.js'; import {defineEmptyBlock} from './test_helpers/block_definitions.js'; import {MockIcon, MockSerializableIcon} from './test_helpers/icon_mocks.js'; import { sharedTestSetup, sharedTestTeardown, } from './test_helpers/setup_teardown.js'; +import {simulateClick} from './test_helpers/user_input.js'; + +class TestIcon extends Blockly.icons.Icon { + showContextMenu(e) { + const menuItems = [ + {text: 'Test icon menu item', enabled: true, callback: () => {}}, + ]; + Blockly.ContextMenu.show( + e, + menuItems, + false, + this.getSourceBlock().workspace, + this.workspaceLocation, + ); + } + + getType() { + new Blockly.icons.IconType('test'); + } +} suite('Icon', function () { setup(function () { @@ -366,4 +387,45 @@ suite('Icon', function () { ); }); }); + + suite('Contextual menus', function () { + setup(function () { + this.workspace = Blockly.inject('blocklyDiv', {}); + Blockly.icons.registry.register( + new Blockly.icons.IconType('test'), + TestIcon, + ); + + this.block = this.workspace.newBlock('empty_block'); + this.block.initSvg(); + }); + + test('are shown when icons are right clicked', function () { + const icon = new TestIcon(this.block); + this.block.addIcon(icon); + simulateClick(icon.getFocusableElement(), {button: 2}); + + const menu = document.querySelector('.blocklyContextMenu'); + assert.isNotNull(menu); + assert.isTrue(menu.innerText.includes('Test icon menu item')); + }); + + test('default to the contextual menu of the parent block', function () { + this.block.setCommentText('hello there'); + const icon = this.block.getIcon(Blockly.icons.IconType.COMMENT); + simulateClick(icon.getFocusableElement(), {button: 2}); + + const expectedItems = + Blockly.ContextMenuRegistry.registry.getContextMenuOptions({ + block: this.block, + }); + + assert.isNotEmpty(expectedItems); + const menu = document.querySelector('.blocklyContextMenu'); + for (const item of expectedItems) { + if (!item.text) continue; + assert.isTrue(menu.innerText.includes(item.text)); + } + }); + }); }); diff --git a/tests/mocha/index.html b/packages/blockly/tests/mocha/index.html similarity index 62% rename from tests/mocha/index.html rename to packages/blockly/tests/mocha/index.html index adc63da4a12..8dd5417ebe0 100644 --- a/tests/mocha/index.html +++ b/packages/blockly/tests/mocha/index.html @@ -13,11 +13,131 @@ visibility: hidden; width: 1000px; } + + .blocklyActiveFocus { + outline-color: #0f0; + outline-width: 2px; + } + .blocklyPassiveFocus { + outline-color: #00f; + outline-width: 1.5px; + } + div.blocklyActiveFocus { + color: #0f0; + } + div.blocklyPassiveFocus { + color: #00f; + } + g.blocklyActiveFocus { + fill: #0f0; + } + g.blocklyPassiveFocus { + fill: #00f; + } - +
+
+ Focusable tree 1 +
+ Tree 1 node 1 +
+ Tree 1 node 1 child 1 +
+ Tree 1 node 1 child 1 child 1 (unregistered) +
+
+
+
+ Tree 1 node 2 +
+ Tree 1 node 2 child 2 (unregistered) +
+
+
+ Tree 1 child 1 (unregistered) +
+
+
+ Focusable tree 2 +
+ Tree 2 node 1 +
+ Nested tree 4 +
+ Tree 4 node 1 (nested) +
+ Tree 4 node 1 child 1 (unregistered) +
+
+
+
+
+ Nested tree 5 +
+ Tree 5 node 1 (nested) +
+
+
+
+ Unregistered tree 3 +
+ Tree 3 node 1 (unregistered) +
+
+
Unfocusable element
+
+ + + + + Group 1 node 1 + + + Tree 1 node 1 child 1 + + + + + Group 1 node 2 + + + + Tree 1 node 2 child 2 (unregistered) + + + + + + + + Group 2 node 1 + + + + + Group 4 node 1 (nested) + + + + + + + + Tree 3 node 1 (unregistered) + + + + + @@ -39,7 +159,6 @@ import {javascriptGenerator} from '../../build/javascript.loader.mjs'; // Import tests. - import './astnode_test.js'; import './block_json_test.js'; import './block_test.js'; import './clipboard_test.js'; @@ -51,6 +170,7 @@ import './contextmenu_items_test.js'; import './contextmenu_test.js'; import './cursor_test.js'; + import './dialog_test.js'; import './dropdowndiv_test.js'; import './event_test.js'; import './event_block_change_test.js'; @@ -68,7 +188,6 @@ import './event_comment_move_test.js'; import './event_comment_drag_test.js'; import './event_comment_resize_test.js'; - import './event_marker_move_test.js'; import './event_selected_test.js'; import './event_theme_change_test.js'; import './event_toolbox_item_select_test.js'; @@ -76,6 +195,7 @@ import './event_var_create_test.js'; import './event_var_delete_test.js'; import './event_var_rename_test.js'; + import './event_var_type_change_test.js'; import './event_viewport_test.js'; import './extensions_test.js'; import './field_checkbox_test.js'; @@ -89,23 +209,26 @@ import './field_textinput_test.js'; import './field_variable_test.js'; import './flyout_test.js'; + import './focus_manager_test.js'; + import './focusable_tree_traverser_test.js'; import './generator_test.js'; import './gesture_test.js'; import './icon_test.js'; import './input_test.js'; import './insertion_marker_test.js'; - import './insertion_marker_manager_test.js'; import './jso_deserialization_test.js'; import './jso_serialization_test.js'; import './json_test.js'; - import './keydown_test.js'; + import './keyboard_navigation_controller_test.js'; import './layering_test.js'; import './blocks/lists_test.js'; import './blocks/logic_ternary_test.js'; import './blocks/loops_test.js'; + import './menu_item_test.js'; import './metrics_test.js'; import './mutator_test.js'; import './names_test.js'; + import './navigation_test.js'; // TODO: Remove these tests. import './old_workspace_comment_test.js'; import './procedure_map_test.js'; @@ -114,9 +237,11 @@ import './registry_test.js'; import './render_management_test.js'; import './serializer_test.js'; + import './shortcut_items_test.js'; import './shortcut_registry_test.js'; import './touch_test.js'; import './theme_test.js'; + import './toast_test.js'; import './toolbox_test.js'; import './tooltip_test.js'; import './trashcan_test.js'; diff --git a/tests/mocha/input_test.js b/packages/blockly/tests/mocha/input_test.js similarity index 99% rename from tests/mocha/input_test.js rename to packages/blockly/tests/mocha/input_test.js index 0c2b0973eaf..dfa30858e0e 100644 --- a/tests/mocha/input_test.js +++ b/packages/blockly/tests/mocha/input_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/insertion_marker_test.js b/packages/blockly/tests/mocha/insertion_marker_test.js similarity index 99% rename from tests/mocha/insertion_marker_test.js rename to packages/blockly/tests/mocha/insertion_marker_test.js index 9ccf8a00b0f..f8215a847eb 100644 --- a/tests/mocha/insertion_marker_test.js +++ b/packages/blockly/tests/mocha/insertion_marker_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/jso_deserialization_test.js b/packages/blockly/tests/mocha/jso_deserialization_test.js similarity index 99% rename from tests/mocha/jso_deserialization_test.js rename to packages/blockly/tests/mocha/jso_deserialization_test.js index dfd3e62b7f2..f6b47d7de6a 100644 --- a/tests/mocha/jso_deserialization_test.js +++ b/packages/blockly/tests/mocha/jso_deserialization_test.js @@ -5,7 +5,7 @@ */ import {EventType} from '../../build/src/core/events/type.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {assertEventFired} from './test_helpers/events.js'; import { MockParameterModel, diff --git a/tests/mocha/jso_serialization_test.js b/packages/blockly/tests/mocha/jso_serialization_test.js similarity index 99% rename from tests/mocha/jso_serialization_test.js rename to packages/blockly/tests/mocha/jso_serialization_test.js index 7cf415e676a..15255545d98 100644 --- a/tests/mocha/jso_serialization_test.js +++ b/packages/blockly/tests/mocha/jso_serialization_test.js @@ -5,7 +5,7 @@ */ import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { defineRowBlock, defineStackBlock, @@ -33,6 +33,7 @@ suite('JSO Serialization', function () { defineStatementBlock(); createGenUidStubWithReturns(new Array(10).fill().map((_, i) => 'id' + i)); + this.variableMap = this.workspace.getVariableMap(); }); teardown(function () { @@ -834,7 +835,7 @@ suite('JSO Serialization', function () { suite('Variables', function () { test('Without type', function () { - this.workspace.createVariable('testVar', '', 'testId'); + this.variableMap.createVariable('testVar', '', 'testId'); const jso = Blockly.serialization.workspaces.save(this.workspace); const variable = jso['variables'][0]; assertProperty(variable, 'name', 'testVar'); @@ -843,7 +844,7 @@ suite('JSO Serialization', function () { }); test('With type', function () { - this.workspace.createVariable('testVar', 'testType', 'testId'); + this.variableMap.createVariable('testVar', 'testType', 'testId'); const jso = Blockly.serialization.workspaces.save(this.workspace); const variable = jso['variables'][0]; assertProperty(variable, 'name', 'testVar'); diff --git a/tests/mocha/json_test.js b/packages/blockly/tests/mocha/json_test.js similarity index 95% rename from tests/mocha/json_test.js rename to packages/blockly/tests/mocha/json_test.js index e9e465c65bc..2e4b68df9ec 100644 --- a/tests/mocha/json_test.js +++ b/packages/blockly/tests/mocha/json_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { addMessageToCleanup, sharedTestSetup, @@ -256,12 +256,6 @@ suite('JSON Block Definitions', function () { 'alt': '%{BKY_ALT_TEXT}', }; const VALUE1 = 'VALUE1'; - const IMAGE2 = { - 'width': 90, - 'height': 123, - 'src': 'http://image2.src', - }; - const VALUE2 = 'VALUE2'; Blockly.defineBlocksWithJsonArray([ { @@ -274,7 +268,6 @@ suite('JSON Block Definitions', function () { 'options': [ [IMAGE0, VALUE0], [IMAGE1, VALUE1], - [IMAGE2, VALUE2], ], }, ], @@ -305,11 +298,6 @@ suite('JSON Block Definitions', function () { assertImageEquals(IMAGE1, image1); assert.equal(image1.alt, IMAGE1_ALT_TEXT); // Via Msg reference assert.equal(VALUE1, options[1][1]); - - const image2 = options[2][0]; - assertImageEquals(IMAGE1, image1); - assert.notExists(image2.alt); // No alt specified. - assert.equal(VALUE2, options[2][1]); }); }); }); diff --git a/packages/blockly/tests/mocha/keyboard_navigation_controller_test.js b/packages/blockly/tests/mocha/keyboard_navigation_controller_test.js new file mode 100644 index 00000000000..dd81e9e4b45 --- /dev/null +++ b/packages/blockly/tests/mocha/keyboard_navigation_controller_test.js @@ -0,0 +1,37 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {assert} from '../../node_modules/chai/index.js'; +import { + sharedTestSetup, + sharedTestTeardown, +} from './test_helpers/setup_teardown.js'; + +suite('Keyboard Navigation Controller', function () { + setup(function () { + sharedTestSetup.call(this); + Blockly.keyboardNavigationController.setIsActive(false); + }); + + teardown(function () { + sharedTestTeardown.call(this); + Blockly.keyboardNavigationController.setIsActive(false); + }); + + test('Setting active keyboard navigation adds css class', function () { + Blockly.keyboardNavigationController.setIsActive(true); + assert.isTrue( + document.body.classList.contains('blocklyKeyboardNavigation'), + ); + }); + + test('Disabling active keyboard navigation removes css class', function () { + Blockly.keyboardNavigationController.setIsActive(false); + assert.isFalse( + document.body.classList.contains('blocklyKeyboardNavigation'), + ); + }); +}); diff --git a/tests/mocha/layering_test.js b/packages/blockly/tests/mocha/layering_test.js similarity index 89% rename from tests/mocha/layering_test.js rename to packages/blockly/tests/mocha/layering_test.js index efc3ef3d632..b84f23252b3 100644 --- a/tests/mocha/layering_test.js +++ b/packages/blockly/tests/mocha/layering_test.js @@ -3,7 +3,7 @@ * Copyright 2023 Google LLC * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, @@ -24,6 +24,15 @@ suite('Layering', function () { const g = Blockly.utils.dom.createSvgElement('g', {}); return { getSvgRoot: () => g, + getFocusableElement: () => { + throw new Error('Unsupported.'); + }, + getFocusableTree: () => { + throw new Error('Unsupported.'); + }, + onNodeFocus: () => {}, + onNodeBlur: () => {}, + canBeFocused: () => false, }; } diff --git a/packages/blockly/tests/mocha/menu_item_test.js b/packages/blockly/tests/mocha/menu_item_test.js new file mode 100644 index 00000000000..7690c0f187e --- /dev/null +++ b/packages/blockly/tests/mocha/menu_item_test.js @@ -0,0 +1,176 @@ +/** + * @license + * Copyright 2026 Raspberry Pi Foundation + * SPDX-License-Identifier: Apache-2.0 + */ + +import {assert} from '../../node_modules/chai/index.js'; +import { + sharedTestSetup, + sharedTestTeardown, +} from './test_helpers/setup_teardown.js'; + +suite('Menu items', function () { + setup(function () { + sharedTestSetup.call(this); + this.menuItem = new Blockly.MenuItem('Hello World'); + this.menuItem.createDom(); + }); + + teardown(function () { + sharedTestTeardown.call(this); + }); + + test('can be RTL', function () { + this.menuItem.setRightToLeft(true); + assert.isTrue( + this.menuItem.getElement().classList.contains('blocklyMenuItemRtl'), + ); + }); + + test('can be LTR', function () { + this.menuItem.setRightToLeft(false); + assert.isFalse( + this.menuItem.getElement().classList.contains('blocklyMenuItemRtl'), + ); + }); + + test('can be checked', function () { + this.menuItem.setCheckable(true); + this.menuItem.setChecked(true); + assert.isTrue( + this.menuItem.getElement().classList.contains('blocklyMenuItemSelected'), + ); + assert.equal( + this.menuItem.getElement().getAttribute('aria-selected'), + 'true', + ); + }); + + test('cannot be checked when designated as uncheckable', function () { + this.menuItem.setCheckable(false); + this.menuItem.setChecked(true); + assert.isFalse( + this.menuItem.getElement().classList.contains('blocklyMenuItemSelected'), + ); + assert.equal( + this.menuItem.getElement().getAttribute('aria-selected'), + 'false', + ); + }); + + test('can be unchecked', function () { + this.menuItem.setCheckable(true); + this.menuItem.setChecked(false); + assert.isFalse( + this.menuItem.getElement().classList.contains('blocklyMenuItemSelected'), + ); + assert.equal( + this.menuItem.getElement().getAttribute('aria-selected'), + 'false', + ); + }); + + test('uncheck themselves when designated as non-checkable', function () { + this.menuItem.setChecked(true); + this.menuItem.setCheckable(false); + + assert.isFalse( + this.menuItem.getElement().classList.contains('blocklyMenuItemSelected'), + ); + assert.equal( + this.menuItem.getElement().getAttribute('aria-selected'), + 'false', + ); + }); + + test('do not check themselves when designated as checkable', function () { + this.menuItem.setChecked(false); + this.menuItem.setCheckable(true); + + assert.isFalse( + this.menuItem.getElement().classList.contains('blocklyMenuItemSelected'), + ); + assert.equal( + this.menuItem.getElement().getAttribute('aria-selected'), + 'false', + ); + }); + + test('adds a checkbox when designated as checkable', function () { + assert.isNull( + this.menuItem.getElement().querySelector('.blocklyMenuItemCheckbox'), + ); + this.menuItem.setCheckable(true); + assert.isNotNull( + this.menuItem.getElement().querySelector('.blocklyMenuItemCheckbox'), + ); + }); + + test('removes the checkbox when designated as uncheckable', function () { + this.menuItem.setCheckable(true); + assert.isNotNull( + this.menuItem.getElement().querySelector('.blocklyMenuItemCheckbox'), + ); + this.menuItem.setCheckable(false); + assert.isNull( + this.menuItem.getElement().querySelector('.blocklyMenuItemCheckbox'), + ); + }); + + test('can be highlighted', function () { + this.menuItem.setHighlighted(true); + assert.isTrue( + this.menuItem.getElement().classList.contains('blocklyMenuItemHighlight'), + ); + }); + + test('can be unhighlighted', function () { + this.menuItem.setHighlighted(false); + assert.isFalse( + this.menuItem.getElement().classList.contains('blocklyMenuItemHighlight'), + ); + }); + + test('cannot be highlighted if not enabled', function () { + this.menuItem.setEnabled(false); + this.menuItem.setHighlighted(true); + assert.isFalse( + this.menuItem.getElement().classList.contains('blocklyMenuItemHighlight'), + ); + }); + + test('can be enabled', function () { + this.menuItem.setEnabled(true); + assert.isTrue(this.menuItem.isEnabled()); + assert.isFalse( + this.menuItem.getElement().classList.contains('blocklyMenuItemDisabled'), + ); + assert.equal( + this.menuItem.getElement().getAttribute('aria-disabled'), + 'false', + ); + }); + + test('can be disabled', function () { + this.menuItem.setEnabled(false); + assert.isFalse(this.menuItem.isEnabled()); + assert.isTrue( + this.menuItem.getElement().classList.contains('blocklyMenuItemDisabled'), + ); + assert.equal( + this.menuItem.getElement().getAttribute('aria-disabled'), + 'true', + ); + }); + + test('invokes its action callback', function () { + let called = false; + const callback = () => { + called = true; + }; + this.menuItem.onAction(callback, this); + this.menuItem.performAction(new Event('click')); + assert.isTrue(called); + }); +}); diff --git a/tests/mocha/metrics_test.js b/packages/blockly/tests/mocha/metrics_test.js similarity index 99% rename from tests/mocha/metrics_test.js rename to packages/blockly/tests/mocha/metrics_test.js index 860e802550d..de9a5641ad7 100644 --- a/tests/mocha/metrics_test.js +++ b/packages/blockly/tests/mocha/metrics_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/mutator_test.js b/packages/blockly/tests/mocha/mutator_test.js similarity index 97% rename from tests/mocha/mutator_test.js rename to packages/blockly/tests/mocha/mutator_test.js index fb6d8caf09b..72b17d0a4bb 100644 --- a/tests/mocha/mutator_test.js +++ b/packages/blockly/tests/mocha/mutator_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { createRenderedBlock, defineMutatorBlocks, diff --git a/tests/mocha/names_test.js b/packages/blockly/tests/mocha/names_test.js similarity index 97% rename from tests/mocha/names_test.js rename to packages/blockly/tests/mocha/names_test.js index 732e28cd57a..e449a59fd6c 100644 --- a/tests/mocha/names_test.js +++ b/packages/blockly/tests/mocha/names_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/packages/blockly/tests/mocha/navigation_test.js b/packages/blockly/tests/mocha/navigation_test.js new file mode 100644 index 00000000000..38dc88894b1 --- /dev/null +++ b/packages/blockly/tests/mocha/navigation_test.js @@ -0,0 +1,876 @@ +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {assert} from '../../node_modules/chai/index.js'; +import { + sharedTestSetup, + sharedTestTeardown, + workspaceTeardown, +} from './test_helpers/setup_teardown.js'; + +suite('Navigation', function () { + setup(function () { + sharedTestSetup.call(this); + Blockly.defineBlocksWithJsonArray([ + { + 'type': 'input_statement', + 'message0': '%1 %2 %3 %4', + 'args0': [ + { + 'type': 'field_input', + 'name': 'NAME', + 'text': 'default', + }, + { + 'type': 'field_input', + 'name': 'NAME', + 'text': 'default', + }, + { + 'type': 'input_value', + 'name': 'NAME', + }, + { + 'type': 'input_statement', + 'name': 'NAME', + }, + ], + 'previousStatement': null, + 'nextStatement': null, + 'colour': 230, + 'tooltip': '', + 'helpUrl': '', + }, + { + 'type': 'value_input', + 'message0': '%1', + 'args0': [ + { + 'type': 'input_value', + 'name': 'NAME', + }, + ], + 'colour': 230, + 'tooltip': '', + 'helpUrl': '', + }, + { + 'type': 'field_input', + 'message0': '%1', + 'args0': [ + { + 'type': 'field_input', + 'name': 'NAME', + 'text': 'default', + }, + ], + 'output': null, + 'colour': 230, + 'tooltip': '', + 'helpUrl': '', + }, + { + 'type': 'double_value_input', + 'message0': '%1 %2', + 'args0': [ + { + 'type': 'input_value', + 'name': 'NAME1', + }, + { + 'type': 'input_value', + 'name': 'NAME2', + }, + ], + }, + ]); + this.workspace = Blockly.inject('blocklyDiv', {}); + this.navigator = this.workspace.getNavigator(); + const statementInput1 = this.workspace.newBlock('input_statement'); + const statementInput2 = this.workspace.newBlock('input_statement'); + const statementInput3 = this.workspace.newBlock('input_statement'); + const statementInput4 = this.workspace.newBlock('input_statement'); + const fieldWithOutput = this.workspace.newBlock('field_input'); + const doubleValueInput = this.workspace.newBlock('double_value_input'); + const valueInput = this.workspace.newBlock('value_input'); + + statementInput1.nextConnection.connect(statementInput2.previousConnection); + statementInput1.inputList[0].connection.connect( + fieldWithOutput.outputConnection, + ); + statementInput2.inputList[1].connection.connect( + statementInput3.previousConnection, + ); + + this.blocks = { + statementInput1: statementInput1, + statementInput2: statementInput2, + statementInput3: statementInput3, + statementInput4: statementInput4, + fieldWithOutput: fieldWithOutput, + valueInput: valueInput, + doubleValueInput, + }; + }); + teardown(function () { + sharedTestTeardown.call(this); + }); + + suite('NavigationFunctions', function () { + setup(function () { + Blockly.defineBlocksWithJsonArray([ + { + 'type': 'top_connection', + 'message0': '', + 'previousStatement': null, + 'colour': 230, + 'tooltip': '', + 'helpUrl': '', + }, + { + 'type': 'start_block', + 'message0': '', + 'nextStatement': null, + 'colour': 230, + 'tooltip': '', + 'helpUrl': '', + }, + { + 'type': 'fields_and_input', + 'message0': '%1 hi %2 %3 %4', + 'args0': [ + { + 'type': 'field_input', + 'name': 'NAME', + 'text': 'default', + }, + { + 'type': 'input_dummy', + }, + { + 'type': 'field_input', + 'name': 'NAME', + 'text': 'default', + }, + { + 'type': 'input_value', + 'name': 'NAME', + }, + ], + 'previousStatement': null, + 'nextStatement': null, + 'colour': 230, + 'tooltip': '', + 'helpUrl': '', + }, + { + 'type': 'two_fields', + 'message0': '%1 hi', + 'args0': [ + { + 'type': 'field_input', + 'name': 'NAME', + 'text': 'default', + }, + ], + 'colour': 230, + 'tooltip': '', + 'helpUrl': '', + }, + { + 'type': 'hidden_field', + 'message0': '%1 %2 %3', + 'args0': [ + { + 'type': 'field_input', + 'name': 'ONE', + 'text': 'default', + }, + { + 'type': 'field_input', + 'name': 'TWO', + 'text': 'default', + }, + { + 'type': 'field_input', + 'name': 'THREE', + 'text': 'default', + }, + ], + 'colour': 230, + 'tooltip': '', + 'helpUrl': '', + }, + { + 'type': 'fields_and_input2', + 'message0': '%1 %2 %3 %4 bye', + 'args0': [ + { + 'type': 'input_value', + 'name': 'NAME', + }, + { + 'type': 'field_input', + 'name': 'NAME', + 'text': 'default', + }, + { + 'type': 'input_value', + 'name': 'NAME', + }, + { + 'type': 'input_statement', + 'name': 'NAME', + }, + ], + 'colour': 230, + 'tooltip': '', + 'helpUrl': '', + }, + { + 'type': 'dummy_input', + 'message0': 'Hello', + 'colour': 230, + 'tooltip': '', + 'helpUrl': '', + }, + { + 'type': 'dummy_inputValue', + 'message0': 'Hello %1 %2', + 'args0': [ + { + 'type': 'input_dummy', + }, + { + 'type': 'input_value', + 'name': 'NAME', + }, + ], + 'colour': 230, + 'tooltip': '', + 'helpUrl': '', + }, + { + 'type': 'output_next', + 'message0': '', + 'output': null, + 'colour': 230, + 'tooltip': '', + 'helpUrl': '', + 'nextStatement': null, + }, + { + 'type': 'hidden_input', + 'message0': '%1 hi %2 %3 %4 %5 %6', + 'args0': [ + { + 'type': 'field_input', + 'name': 'ONE', + 'text': 'default', + }, + { + 'type': 'input_dummy', + }, + { + 'type': 'field_input', + 'name': 'TWO', + 'text': 'default', + }, + { + 'type': 'input_value', + 'name': 'SECOND', + }, + { + 'type': 'field_input', + 'name': 'THREE', + 'text': 'default', + }, + { + 'type': 'input_value', + 'name': 'THIRD', + }, + ], + 'previousStatement': null, + 'nextStatement': null, + 'colour': 230, + 'tooltip': '', + 'helpUrl': '', + }, + { + 'type': 'buttons', + 'message0': 'If %1 %2 Then %3 %4 more %5 %6 %7', + 'args0': [ + { + 'type': 'field_image', + 'name': 'BUTTON1', + 'src': 'https://www.gstatic.com/codesite/ph/images/star_on.gif', + 'width': 30, + 'height': 30, + 'alt': '*', + }, + { + 'type': 'input_value', + 'name': 'VALUE1', + 'check': '', + }, + { + 'type': 'field_image', + 'name': 'BUTTON2', + 'src': 'https://www.gstatic.com/codesite/ph/images/star_on.gif', + 'width': 30, + 'height': 30, + 'alt': '*', + }, + { + 'type': 'input_dummy', + 'name': 'DUMMY1', + 'check': '', + }, + { + 'type': 'input_value', + 'name': 'VALUE2', + 'check': '', + }, + { + 'type': 'input_statement', + 'name': 'STATEMENT1', + 'check': 'Number', + }, + { + 'type': 'field_image', + 'name': 'BUTTON3', + 'src': 'https://www.gstatic.com/codesite/ph/images/star_on.gif', + 'width': 30, + 'height': 30, + 'alt': '*', + }, + ], + 'previousStatement': null, + 'nextStatement': null, + 'colour': 230, + 'tooltip': '', + 'helpUrl': '', + }, + ]); + const noNextConnection = this.workspace.newBlock('top_connection'); + const fieldAndInputs = this.workspace.newBlock('fields_and_input'); + const twoFields = this.workspace.newBlock('two_fields'); + const fieldAndInputs2 = this.workspace.newBlock('fields_and_input2'); + const noPrevConnection = this.workspace.newBlock('start_block'); + const hiddenField = this.workspace.newBlock('hidden_field'); + const hiddenInput = this.workspace.newBlock('hidden_input'); + this.blocks.noNextConnection = noNextConnection; + this.blocks.fieldAndInputs = fieldAndInputs; + this.blocks.twoFields = twoFields; + this.blocks.fieldAndInputs2 = fieldAndInputs2; + this.blocks.noPrevConnection = noPrevConnection; + this.blocks.hiddenField = hiddenField; + this.blocks.hiddenInput = hiddenInput; + + hiddenField.inputList[0].fieldRow[1].setVisible(false); + hiddenInput.inputList[1].setVisible(false); + + const dummyInput = this.workspace.newBlock('dummy_input'); + const dummyInputValue = this.workspace.newBlock('dummy_inputValue'); + const fieldWithOutput2 = this.workspace.newBlock('field_input'); + this.blocks.dummyInput = dummyInput; + this.blocks.dummyInputValue = dummyInputValue; + this.blocks.fieldWithOutput2 = fieldWithOutput2; + + const secondBlock = this.workspace.newBlock('input_statement'); + const outputNextBlock = this.workspace.newBlock('output_next'); + this.blocks.secondBlock = secondBlock; + this.blocks.outputNextBlock = outputNextBlock; + + const buttonBlock = this.workspace.newBlock('buttons', 'button_block'); + const buttonInput1 = this.workspace.newBlock( + 'field_input', + 'button_input1', + ); + const buttonInput2 = this.workspace.newBlock( + 'field_input', + 'button_input2', + ); + const buttonNext = this.workspace.newBlock( + 'input_statement', + 'button_next', + ); + buttonBlock.inputList[0].connection.connect( + buttonInput1.outputConnection, + ); + buttonBlock.inputList[2].connection.connect( + buttonInput2.outputConnection, + ); + buttonBlock.nextConnection.connect(buttonNext.previousConnection); + // Make buttons by adding a click handler + const clickHandler = function () { + return; + }; + buttonBlock.getField('BUTTON1').setOnClickHandler(clickHandler); + buttonBlock.getField('BUTTON2').setOnClickHandler(clickHandler); + buttonBlock.getField('BUTTON3').setOnClickHandler(clickHandler); + this.blocks.buttonBlock = buttonBlock; + this.blocks.buttonInput1 = buttonInput1; + this.blocks.buttonInput2 = buttonInput2; + this.blocks.buttonNext = buttonNext; + + this.workspace.cleanUp(); + }); + suite('Next', function () { + setup(function () { + this.singleBlockWorkspace = new Blockly.Workspace(); + const singleBlock = this.singleBlockWorkspace.newBlock('two_fields'); + this.blocks.singleBlock = singleBlock; + }); + teardown(function () { + workspaceTeardown.call(this, this.singleBlockWorkspace); + }); + + test('fromPreviousToBlock', function () { + const prevConnection = this.blocks.statementInput1.previousConnection; + const nextNode = this.navigator.getNextSibling(prevConnection); + assert.equal(nextNode, this.blocks.statementInput1); + }); + test('fromBlockToNextBlock', function () { + const nextNode = this.navigator.getNextSibling( + this.blocks.statementInput1, + ); + assert.equal(nextNode, this.blocks.statementInput2); + }); + test('fromNextToPrevious', function () { + const nextConnection = this.blocks.statementInput1.nextConnection; + const prevConnection = this.blocks.statementInput2.previousConnection; + const nextNode = this.navigator.getNextSibling(nextConnection); + assert.equal(nextNode, prevConnection); + }); + test('fromInputToInput', function () { + const input = this.blocks.doubleValueInput.inputList[0]; + const inputConnection = + this.blocks.doubleValueInput.inputList[1].connection; + const nextNode = this.navigator.getNextSibling(input.connection); + assert.equal(nextNode, inputConnection); + }); + test('fromInputToField', function () { + const input = this.blocks.fieldAndInputs2.inputList[0]; + const field = this.blocks.fieldAndInputs2.inputList[1].fieldRow[0]; + const nextNode = this.navigator.getNextSibling(input.connection); + assert.equal(nextNode, field); + }); + test('fromInputToNull', function () { + const input = this.blocks.fieldAndInputs2.inputList[2]; + const nextNode = this.navigator.getNextSibling(input.connection); + assert.isNull(nextNode); + }); + test('fromOutputToBlock', function () { + const output = this.blocks.fieldWithOutput.outputConnection; + const nextNode = this.navigator.getNextSibling(output); + assert.equal(nextNode, this.blocks.fieldWithOutput); + }); + test('fromFieldToNestedBlock', function () { + const field = this.blocks.statementInput1.inputList[0].fieldRow[1]; + const inputConnection = + this.blocks.statementInput1.inputList[0].connection; + const nextNode = this.navigator.getNextSibling(field); + assert.equal(nextNode, this.blocks.fieldWithOutput); + }); + test('fromFieldToField', function () { + const field = this.blocks.fieldAndInputs.inputList[0].fieldRow[0]; + const field2 = this.blocks.fieldAndInputs.inputList[1].fieldRow[0]; + const nextNode = this.navigator.getNextSibling(field); + assert.equal(nextNode, field2); + }); + test('fromFieldToNull', function () { + const field = this.blocks.twoFields.inputList[0].fieldRow[0]; + const nextNode = this.navigator.getNextSibling(field); + assert.isNull(nextNode); + }); + test('skipsHiddenField', function () { + const field = this.blocks.hiddenField.inputList[0].fieldRow[0]; + const field2 = this.blocks.hiddenField.inputList[0].fieldRow[2]; + const nextNode = this.navigator.getNextSibling(field); + assert.equal(nextNode.name, field2.name); + }); + test('skipsHiddenInput', function () { + const field = this.blocks.hiddenInput.inputList[0].fieldRow[0]; + const nextNode = this.navigator.getNextSibling(field); + assert.equal( + nextNode, + this.blocks.hiddenInput.inputList[2].fieldRow[0], + ); + }); + test('from icon to icon', function () { + this.blocks.statementInput1.setCommentText('test'); + this.blocks.statementInput1.setWarningText('test'); + const icons = this.blocks.statementInput1.getIcons(); + const nextNode = this.navigator.getNextSibling(icons[0]); + assert.equal(nextNode, icons[1]); + }); + test('from icon to field', function () { + this.blocks.statementInput1.setCommentText('test'); + this.blocks.statementInput1.setWarningText('test'); + const icons = this.blocks.statementInput1.getIcons(); + const nextNode = this.navigator.getNextSibling(icons[1]); + assert.equal( + nextNode, + this.blocks.statementInput1.inputList[0].fieldRow[0], + ); + }); + test('from icon to null', function () { + this.blocks.dummyInput.setCommentText('test'); + const icons = this.blocks.dummyInput.getIcons(); + const nextNode = this.navigator.getNextSibling(icons[0]); + assert.isNull(nextNode); + }); + test('fromBlockToFieldInNextInput', function () { + const field = this.blocks.buttonBlock.getField('BUTTON2'); + const nextNode = this.navigator.getNextSibling( + this.blocks.buttonInput1, + ); + assert.equal(nextNode, field); + }); + test('fromBlockToFieldSkippingInput', function () { + const field = this.blocks.buttonBlock.getField('BUTTON3'); + const nextNode = this.navigator.getNextSibling( + this.blocks.buttonInput2, + ); + assert.equal(nextNode, field); + }); + test('skipsChildrenOfCollapsedBlocks', function () { + this.blocks.buttonBlock.setCollapsed(true); + const nextNode = this.navigator.getNextSibling(this.blocks.buttonBlock); + assert.equal(nextNode.id, this.blocks.buttonNext.id); + }); + test('fromFieldSkipsHiddenInputs', function () { + this.blocks.buttonBlock.inputList[2].setVisible(false); + const fieldStart = this.blocks.buttonBlock.getField('BUTTON2'); + const fieldEnd = this.blocks.buttonBlock.getField('BUTTON3'); + const nextNode = this.navigator.getNextSibling(fieldStart); + assert.equal(nextNode.name, fieldEnd.name); + }); + }); + + suite('Previous', function () { + test('fromPreviousToNext', function () { + const prevConnection = this.blocks.statementInput2.previousConnection; + const prevNode = this.navigator.getPreviousSibling(prevConnection); + const nextConnection = this.blocks.statementInput1.nextConnection; + assert.equal(prevNode, nextConnection); + }); + test('fromPreviousToInput', function () { + const prevConnection = this.blocks.statementInput3.previousConnection; + const prevNode = this.navigator.getPreviousSibling(prevConnection); + assert.isNull(prevNode); + }); + test('fromBlockToPrevious', function () { + const prevNode = this.navigator.getPreviousSibling( + this.blocks.statementInput2, + ); + const previousBlock = this.blocks.statementInput1; + assert.equal(prevNode, previousBlock); + }); + test('fromOutputBlockToPreviousField', function () { + const prevNode = this.navigator.getPreviousSibling( + this.blocks.fieldWithOutput, + ); + const outputConnection = this.blocks.fieldWithOutput.outputConnection; + assert.equal(prevNode, [...this.blocks.statementInput1.getFields()][1]); + }); + test('fromNextToBlock', function () { + const nextConnection = this.blocks.statementInput1.nextConnection; + const prevNode = this.navigator.getPreviousSibling(nextConnection); + assert.equal(prevNode, this.blocks.statementInput1); + }); + test('fromInputToField', function () { + // Disconnect the block that was connected to the input we're testing, + // because we only navigate to/from empty input connections (if they're + // connected navigation targets the connected block, bypassing the + // connection). + this.blocks.fieldWithOutput.outputConnection.disconnect(); + const input = this.blocks.statementInput1.inputList[0]; + const prevNode = this.navigator.getPreviousSibling(input.connection); + assert.equal(prevNode, input.fieldRow[1]); + }); + test('fromInputToNull', function () { + const input = this.blocks.fieldAndInputs2.inputList[0]; + const prevNode = this.navigator.getPreviousSibling(input.connection); + assert.isNull(prevNode); + }); + test('fromInputToInput', function () { + const input = this.blocks.doubleValueInput.inputList[1]; + const inputConnection = + this.blocks.doubleValueInput.inputList[0].connection; + const prevNode = this.navigator.getPreviousSibling(input.connection); + assert.equal(prevNode, inputConnection); + }); + test('fromOutputToNull', function () { + const output = this.blocks.fieldWithOutput.outputConnection; + const prevNode = this.navigator.getPreviousSibling(output); + assert.isNull(prevNode); + }); + test('fromFieldToNull', function () { + const field = this.blocks.statementInput1.inputList[0].fieldRow[0]; + const prevNode = this.navigator.getPreviousSibling(field); + assert.isNull(prevNode); + }); + test('fromFieldToInput', function () { + const outputBlock = this.workspace.newBlock('field_input'); + this.blocks.fieldAndInputs2.inputList[0].connection.connect( + outputBlock.outputConnection, + ); + + const field = this.blocks.fieldAndInputs2.inputList[1].fieldRow[0]; + const inputConnection = + this.blocks.fieldAndInputs2.inputList[0].connection; + const prevNode = this.navigator.getPreviousSibling(field); + assert.equal(prevNode, outputBlock); + }); + test('fromFieldToField', function () { + const field = this.blocks.fieldAndInputs.inputList[1].fieldRow[0]; + const field2 = this.blocks.fieldAndInputs.inputList[0].fieldRow[0]; + const prevNode = this.navigator.getPreviousSibling(field); + assert.equal(prevNode, field2); + }); + test('skipsHiddenField', function () { + const field = this.blocks.hiddenField.inputList[0].fieldRow[2]; + const field2 = this.blocks.hiddenField.inputList[0].fieldRow[0]; + const prevNode = this.navigator.getPreviousSibling(field); + assert.equal(prevNode.name, field2.name); + }); + test('skipsHiddenInput', function () { + const field = this.blocks.hiddenInput.inputList[2].fieldRow[0]; + const nextNode = this.navigator.getPreviousSibling(field); + assert.equal( + nextNode, + this.blocks.hiddenInput.inputList[0].fieldRow[0], + ); + }); + test('from icon to icon', function () { + this.blocks.statementInput1.setCommentText('test'); + this.blocks.statementInput1.setWarningText('test'); + const icons = this.blocks.statementInput1.getIcons(); + const prevNode = this.navigator.getPreviousSibling(icons[1]); + assert.equal(prevNode, icons[0]); + }); + test('from field to icon', function () { + this.blocks.statementInput1.setCommentText('test'); + this.blocks.statementInput1.setWarningText('test'); + const icons = this.blocks.statementInput1.getIcons(); + const prevNode = this.navigator.getPreviousSibling( + this.blocks.statementInput1.inputList[0].fieldRow[0], + ); + assert.equal(prevNode, icons[1]); + }); + test('from icon to null', function () { + this.blocks.dummyInput.setCommentText('test'); + const icons = this.blocks.dummyInput.getIcons(); + const prevNode = this.navigator.getPreviousSibling(icons[0]); + assert.isNull(prevNode); + }); + test('fromBlockToFieldInSameInput', function () { + const field = this.blocks.buttonBlock.getField('BUTTON1'); + const prevNode = this.navigator.getPreviousSibling( + this.blocks.buttonInput1, + ); + assert.equal(prevNode, field); + }); + test('fromBlockToFieldInPrevInput', function () { + const field = this.blocks.buttonBlock.getField('BUTTON2'); + const prevNode = this.navigator.getPreviousSibling( + this.blocks.buttonInput2, + ); + assert.equal(prevNode, field); + }); + test('skipsChildrenOfCollapsedBlocks', function () { + this.blocks.buttonBlock.setCollapsed(true); + const prevNode = this.navigator.getPreviousSibling( + this.blocks.buttonNext, + ); + assert.equal(prevNode.id, this.blocks.buttonBlock.id); + }); + test('fromFieldSkipsHiddenInputs', function () { + this.blocks.buttonBlock.inputList[2].setVisible(false); + const fieldStart = this.blocks.buttonBlock.getField('BUTTON3'); + const fieldEnd = this.blocks.buttonBlock.getField('BUTTON2'); + const nextNode = this.navigator.getPreviousSibling(fieldStart); + assert.equal(nextNode.name, fieldEnd.name); + }); + }); + + suite('In', function () { + setup(function () { + this.emptyWorkspace = Blockly.inject(document.createElement('div'), {}); + }); + teardown(function () { + workspaceTeardown.call(this, this.emptyWorkspace); + }); + + test('fromInputToOutput', function () { + const input = this.blocks.statementInput1.inputList[0]; + const inNode = this.navigator.getFirstChild(input.connection); + const outputConnection = this.blocks.fieldWithOutput.outputConnection; + assert.equal(inNode, outputConnection); + }); + test('fromInputToNull', function () { + const input = this.blocks.statementInput2.inputList[0]; + const inNode = this.navigator.getFirstChild(input.connection); + assert.isNull(inNode); + }); + test('fromInputToPrevious', function () { + const input = this.blocks.statementInput2.inputList[1]; + const previousConnection = + this.blocks.statementInput3.previousConnection; + const inNode = this.navigator.getFirstChild(input.connection); + assert.equal(inNode, previousConnection); + }); + test('fromBlockToInput', function () { + const connection = this.blocks.valueInput.inputList[0].connection; + const inNode = this.navigator.getFirstChild(this.blocks.valueInput); + assert.equal(inNode, connection); + }); + test('fromBlockToField', function () { + const inNode = this.navigator.getFirstChild( + this.blocks.statementInput1, + ); + const field = this.blocks.statementInput1.inputList[0].fieldRow[0]; + assert.equal(inNode, field); + }); + test('fromBlockToNull_DummyInput', function () { + const inNode = this.navigator.getFirstChild(this.blocks.dummyInput); + assert.isNull(inNode); + }); + test('fromBlockToInput_DummyInputValue', function () { + const inNode = this.navigator.getFirstChild( + this.blocks.dummyInputValue, + ); + assert.equal( + inNode, + this.blocks.dummyInputValue.inputList[1].connection, + ); + }); + test('fromOuputToNull', function () { + const output = this.blocks.fieldWithOutput.outputConnection; + const inNode = this.navigator.getFirstChild(output); + assert.isNull(inNode); + }); + test('fromFieldToNull', function () { + const field = this.blocks.statementInput1.inputList[0].fieldRow[0]; + const inNode = this.navigator.getFirstChild(field); + assert.isNull(inNode); + }); + test('fromWorkspaceToBlock', function () { + const inNode = this.navigator.getFirstChild(this.workspace); + assert.equal(inNode, this.workspace.getTopBlocks(true)[0]); + }); + test('fromWorkspaceToNull', function () { + const inNode = this.navigator.getFirstChild(this.emptyWorkspace); + assert.isNull(inNode); + }); + test('from block to icon', function () { + this.blocks.dummyInput.setCommentText('test'); + const icons = this.blocks.dummyInput.getIcons(); + const inNode = this.navigator.getFirstChild(this.blocks.dummyInput); + assert.equal(inNode, icons[0]); + }); + test('from icon to null', function () { + this.blocks.dummyInput.setCommentText('test'); + const icons = this.blocks.dummyInput.getIcons(); + const inNode = this.navigator.getFirstChild(icons[0]); + assert.isNull(inNode); + }); + test('skipsChildrenOfCollapsedBlocks', function () { + this.blocks.buttonBlock.setCollapsed(true); + const inNode = this.navigator.getFirstChild(this.blocks.buttonBlock); + assert.isNull(inNode); + }); + }); + + suite('Out', function () { + setup(function () { + const secondBlock = this.blocks.secondBlock; + const outputNextBlock = this.blocks.outputNextBlock; + this.blocks.noPrevConnection.nextConnection.connect( + secondBlock.previousConnection, + ); + secondBlock.inputList[0].connection.connect( + outputNextBlock.outputConnection, + ); + }); + + test('fromInputToBlock', function () { + const input = this.blocks.statementInput1.inputList[0]; + const outNode = this.navigator.getParent(input.connection); + assert.equal(outNode, this.blocks.statementInput1); + }); + test('fromOutputToBlock', function () { + const output = this.blocks.fieldWithOutput.outputConnection; + const outNode = this.navigator.getParent(output); + assert.equal(outNode, this.blocks.fieldWithOutput); + }); + test('fromOutputToBlock', function () { + const output = this.blocks.fieldWithOutput2.outputConnection; + const outNode = this.navigator.getParent(output); + assert.equal(outNode, this.blocks.fieldWithOutput2); + }); + test('fromFieldToBlock', function () { + const field = this.blocks.statementInput1.inputList[0].fieldRow[0]; + const outNode = this.navigator.getParent(field); + assert.equal(outNode, this.blocks.statementInput1); + }); + test('fromPreviousToBlock', function () { + const previous = this.blocks.statementInput2.previousConnection; + const outNode = this.navigator.getParent(previous); + assert.equal(outNode, this.blocks.statementInput2); + }); + test('fromNextToBlock', function () { + const next = this.blocks.statementInput2.nextConnection; + const outNode = this.navigator.getParent(next); + assert.equal(outNode, this.blocks.statementInput2); + }); + test('fromNextToBlock_NoPreviousConnection', function () { + const next = this.blocks.secondBlock.nextConnection; + const outNode = this.navigator.getParent(next); + assert.equal(outNode, this.blocks.secondBlock); + }); + /** + * This is where there is a block with both an output connection and a + * next connection attached to an input. + */ + test('fromNextToBlock_OutputAndPreviousConnection', function () { + const next = this.blocks.outputNextBlock.nextConnection; + const outNode = this.navigator.getParent(next); + assert.equal(outNode, this.blocks.outputNextBlock); + }); + test('fromBlockToWorkspace', function () { + const outNode = this.navigator.getParent(this.blocks.statementInput2); + assert.equal(outNode, this.workspace); + }); + test('fromBlockToEnclosingStatement', function () { + const enclosingStatement = this.blocks.statementInput2; + const outNode = this.navigator.getParent(this.blocks.statementInput3); + assert.equal(outNode, enclosingStatement); + }); + test('fromTopBlockToWorkspace', function () { + const outNode = this.navigator.getParent(this.blocks.statementInput1); + assert.equal(outNode, this.workspace); + }); + test('fromOutputBlockToWorkspace', function () { + const outNode = this.navigator.getParent(this.blocks.fieldWithOutput2); + assert.equal(outNode, this.workspace); + }); + test('fromOutputNextBlockToWorkspace', function () { + const inputConnection = this.blocks.secondBlock; + const outNode = this.navigator.getParent(this.blocks.outputNextBlock); + assert.equal(outNode, inputConnection); + }); + test('from icon to block', function () { + this.blocks.dummyInput.setCommentText('test'); + const icons = this.blocks.dummyInput.getIcons(); + const outNode = this.navigator.getParent(icons[0]); + assert.equal(outNode, this.blocks.dummyInput); + }); + }); + }); +}); diff --git a/tests/mocha/old_workspace_comment_test.js b/packages/blockly/tests/mocha/old_workspace_comment_test.js similarity index 99% rename from tests/mocha/old_workspace_comment_test.js rename to packages/blockly/tests/mocha/old_workspace_comment_test.js index 08a2523f50e..5038d67bad8 100644 --- a/tests/mocha/old_workspace_comment_test.js +++ b/packages/blockly/tests/mocha/old_workspace_comment_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/procedure_map_test.js b/packages/blockly/tests/mocha/procedure_map_test.js similarity index 96% rename from tests/mocha/procedure_map_test.js rename to packages/blockly/tests/mocha/procedure_map_test.js index eebd5a9f326..5e29c6ca050 100644 --- a/tests/mocha/procedure_map_test.js +++ b/packages/blockly/tests/mocha/procedure_map_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {MockProcedureModel} from './test_helpers/procedures.js'; import { sharedTestSetup, diff --git a/tests/mocha/rect_test.js b/packages/blockly/tests/mocha/rect_test.js similarity index 99% rename from tests/mocha/rect_test.js rename to packages/blockly/tests/mocha/rect_test.js index 37712dff3a0..652837eaef6 100644 --- a/tests/mocha/rect_test.js +++ b/packages/blockly/tests/mocha/rect_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/registry_test.js b/packages/blockly/tests/mocha/registry_test.js similarity index 99% rename from tests/mocha/registry_test.js rename to packages/blockly/tests/mocha/registry_test.js index 6bcb8b5b077..2d5e22543ca 100644 --- a/tests/mocha/registry_test.js +++ b/packages/blockly/tests/mocha/registry_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/render_management_test.js b/packages/blockly/tests/mocha/render_management_test.js similarity index 98% rename from tests/mocha/render_management_test.js rename to packages/blockly/tests/mocha/render_management_test.js index 4de0635394b..94fa48805cf 100644 --- a/tests/mocha/render_management_test.js +++ b/packages/blockly/tests/mocha/render_management_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/serializer_test.js b/packages/blockly/tests/mocha/serializer_test.js similarity index 99% rename from tests/mocha/serializer_test.js rename to packages/blockly/tests/mocha/serializer_test.js index a3a3761e9c6..efd1f308b01 100644 --- a/tests/mocha/serializer_test.js +++ b/packages/blockly/tests/mocha/serializer_test.js @@ -5,7 +5,7 @@ */ import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { TestCase, TestSuite, diff --git a/tests/mocha/keydown_test.js b/packages/blockly/tests/mocha/shortcut_items_test.js similarity index 54% rename from tests/mocha/keydown_test.js rename to packages/blockly/tests/mocha/shortcut_items_test.js index 0b72a7fee6b..dfbae3f0901 100644 --- a/tests/mocha/keydown_test.js +++ b/packages/blockly/tests/mocha/shortcut_items_test.js @@ -5,6 +5,7 @@ */ import * as Blockly from '../../build/src/core/blockly.js'; +import {assert} from '../../node_modules/chai/index.js'; import {defineStackBlock} from './test_helpers/block_definitions.js'; import { sharedTestSetup, @@ -12,7 +13,7 @@ import { } from './test_helpers/setup_teardown.js'; import {createKeyDownEvent} from './test_helpers/user_input.js'; -suite('Key Down', function () { +suite('Keyboard Shortcut Items', function () { setup(function () { sharedTestSetup.call(this); this.workspace = Blockly.inject('blocklyDiv', {}); @@ -31,9 +32,32 @@ suite('Key Down', function () { defineStackBlock(); const block = workspace.newBlock('stack_block'); Blockly.common.setSelected(block); + sinon.stub(Blockly.getFocusManager(), 'getFocusedNode').returns(block); return block; } + /** + * Creates a block and sets its nextConnection as the focused node. + * @param {Blockly.Workspace} workspace The workspace to create a new block on. + */ + function setSelectedConnection(workspace) { + defineStackBlock(); + const block = workspace.newBlock('stack_block'); + sinon + .stub(Blockly.getFocusManager(), 'getFocusedNode') + .returns(block.nextConnection); + } + + /** + * Creates a workspace comment and set it as the focused node. + * @param {Blockly.Workspace} workspace The workspace to create a new comment on. + */ + function setSelectedComment(workspace) { + const comment = workspace.newComment(); + sinon.stub(Blockly.getFocusManager(), 'getFocusedNode').returns(comment); + return comment; + } + /** * Creates a test for not running keyDown events when the workspace is in read only mode. * @param {Object} keyEvent Mocked key down event. Use createKeyDownEvent. @@ -42,7 +66,7 @@ suite('Key Down', function () { function runReadOnlyTest(keyEvent, opt_name) { const name = opt_name ? opt_name : 'Not called when readOnly is true'; test(name, function () { - this.workspace.options.readOnly = true; + this.workspace.setIsReadOnly(true); this.injectionDiv.dispatchEvent(keyEvent); sinon.assert.notCalled(this.hideChaffSpy); }); @@ -72,9 +96,14 @@ suite('Key Down', function () { this.injectionDiv.dispatchEvent(this.event); sinon.assert.notCalled(this.hideChaffSpy); }); + test('Called when connection is focused', function () { + setSelectedConnection(this.workspace); + this.injectionDiv.dispatchEvent(this.event); + sinon.assert.calledOnce(this.hideChaffSpy); + }); }); - suite('Delete Block', function () { + suite('Delete', function () { setup(function () { this.hideChaffSpy = sinon.spy( Blockly.WorkspaceSvg.prototype, @@ -88,6 +117,7 @@ suite('Key Down', function () { ['Backspace', createKeyDownEvent(Blockly.utils.KeyCodes.BACKSPACE)], ]; // Delete a block. + // Note that chaff is hidden when a block is deleted. suite('Simple', function () { testCases.forEach(function (testCase) { const testCaseName = testCase[0]; @@ -107,6 +137,16 @@ suite('Key Down', function () { runReadOnlyTest(keyEvent, testCaseName); }); }); + // Do not delete anything if a connection is focused. + test('Not called when connection is focused', function () { + // Restore the stub behavior called during setup + Blockly.getFocusManager().getFocusedNode.restore(); + + setSelectedConnection(this.workspace); + const event = createKeyDownEvent(Blockly.utils.KeyCodes.DELETE); + this.injectionDiv.dispatchEvent(event); + sinon.assert.notCalled(this.hideChaffSpy); + }); }); suite('Copy', function () { @@ -131,14 +171,130 @@ suite('Key Down', function () { Blockly.utils.KeyCodes.META, ]), ], + ]; + // Copy a block. + suite('Simple', function () { + testCases.forEach(function (testCase) { + const testCaseName = testCase[0]; + const keyEvent = testCase[1]; + test(testCaseName, function () { + this.injectionDiv.dispatchEvent(keyEvent); + sinon.assert.calledOnce(this.copySpy); + sinon.assert.calledOnce(this.hideChaffSpy); + }); + }); + }); + // Allow copying a block if a workspace is in readonly mode. + suite('Called when readOnly is true', function () { + testCases.forEach(function (testCase) { + const testCaseName = testCase[0]; + const keyEvent = testCase[1]; + test(testCaseName, function () { + this.workspace.setIsReadOnly(true); + this.injectionDiv.dispatchEvent(keyEvent); + sinon.assert.calledOnce(this.copySpy); + sinon.assert.calledOnce(this.hideChaffSpy); + }); + }); + }); + // Do not copy a block if a drag is in progress. + suite('Drag in progress', function () { + testCases.forEach(function (testCase) { + const testCaseName = testCase[0]; + const keyEvent = testCase[1]; + test(testCaseName, function () { + sinon.stub(this.workspace, 'isDragging').returns(true); + this.injectionDiv.dispatchEvent(keyEvent); + sinon.assert.notCalled(this.copySpy); + sinon.assert.notCalled(this.hideChaffSpy); + }); + }); + }); + // Do not copy a block if is is not deletable. + suite('Block is not deletable', function () { + testCases.forEach(function (testCase) { + const testCaseName = testCase[0]; + const keyEvent = testCase[1]; + test(testCaseName, function () { + sinon + .stub(Blockly.common.getSelected(), 'isOwnDeletable') + .returns(false); + this.injectionDiv.dispatchEvent(keyEvent); + sinon.assert.notCalled(this.copySpy); + sinon.assert.notCalled(this.hideChaffSpy); + }); + }); + }); + // Do not copy a block if it is not movable. + suite('Block is not movable', function () { + testCases.forEach(function (testCase) { + const testCaseName = testCase[0]; + const keyEvent = testCase[1]; + test(testCaseName, function () { + sinon + .stub(Blockly.common.getSelected(), 'isOwnMovable') + .returns(false); + this.injectionDiv.dispatchEvent(keyEvent); + sinon.assert.notCalled(this.copySpy); + sinon.assert.notCalled(this.hideChaffSpy); + }); + }); + }); + test('Not called when connection is focused', function () { + // Restore the stub behavior called during setup + Blockly.getFocusManager().getFocusedNode.restore(); + + setSelectedConnection(this.workspace); + const event = createKeyDownEvent(Blockly.utils.KeyCodes.C, [ + Blockly.utils.KeyCodes.CTRL, + ]); + this.injectionDiv.dispatchEvent(event); + sinon.assert.notCalled(this.copySpy); + sinon.assert.notCalled(this.hideChaffSpy); + }); + // Copy a comment. + test('Workspace comment', function () { + testCases.forEach(function (testCase) { + const testCaseName = testCase[0]; + const keyEvent = testCase[1]; + test(testCaseName, function () { + Blockly.getFocusManager().getFocusedNode.restore(); + this.comment = setSelectedComment(this.workspace); + this.copySpy = sinon.spy(this.comment, 'toCopyData'); + + this.injectionDiv.dispatchEvent(keyEvent); + sinon.assert.calledOnce(this.copySpy); + sinon.assert.calledOnce(this.hideChaffSpy); + }); + }); + }); + }); + + suite('Cut', function () { + setup(function () { + this.block = setSelectedBlock(this.workspace); + this.copySpy = sinon.spy(this.block, 'toCopyData'); + this.disposeSpy = sinon.spy(this.block, 'dispose'); + this.hideChaffSpy = sinon.spy( + Blockly.WorkspaceSvg.prototype, + 'hideChaff', + ); + }); + const testCases = [ [ - 'Alt C', - createKeyDownEvent(Blockly.utils.KeyCodes.C, [ - Blockly.utils.KeyCodes.ALT, + 'Control X', + createKeyDownEvent(Blockly.utils.KeyCodes.X, [ + Blockly.utils.KeyCodes.CTRL, + ]), + ], + [ + 'Meta X', + createKeyDownEvent(Blockly.utils.KeyCodes.X, [ + Blockly.utils.KeyCodes.META, ]), ], ]; - // Copy a block. + // Cut a block. suite('Simple', function () { testCases.forEach(function (testCase) { const testCaseName = testCase[0]; @@ -146,59 +302,115 @@ suite('Key Down', function () { test(testCaseName, function () { this.injectionDiv.dispatchEvent(keyEvent); sinon.assert.calledOnce(this.copySpy); + sinon.assert.calledOnce(this.disposeSpy); sinon.assert.calledOnce(this.hideChaffSpy); }); }); }); - // Do not copy a block if a workspace is in readonly mode. + // Do not cut a block if a workspace is in readonly mode. suite('Not called when readOnly is true', function () { testCases.forEach(function (testCase) { const testCaseName = testCase[0]; const keyEvent = testCase[1]; - runReadOnlyTest(keyEvent, testCaseName); + test(testCaseName, function () { + this.workspace.setIsReadOnly(true); + this.injectionDiv.dispatchEvent(keyEvent); + sinon.assert.notCalled(this.copySpy); + sinon.assert.notCalled(this.disposeSpy); + sinon.assert.notCalled(this.hideChaffSpy); + }); }); }); - // Do not copy a block if a gesture is in progress. - suite('Gesture in progress', function () { + // Do not cut a block if a drag is in progress. + suite('Drag in progress', function () { testCases.forEach(function (testCase) { const testCaseName = testCase[0]; const keyEvent = testCase[1]; test(testCaseName, function () { - sinon.stub(Blockly.Gesture, 'inProgress').returns(true); + sinon.stub(this.workspace, 'isDragging').returns(true); this.injectionDiv.dispatchEvent(keyEvent); sinon.assert.notCalled(this.copySpy); + sinon.assert.notCalled(this.disposeSpy); sinon.assert.notCalled(this.hideChaffSpy); }); }); }); - // Do not copy a block if is is not deletable. + // Do not cut a block if is is not deletable. suite('Block is not deletable', function () { testCases.forEach(function (testCase) { const testCaseName = testCase[0]; const keyEvent = testCase[1]; test(testCaseName, function () { sinon - .stub(Blockly.common.getSelected(), 'isDeletable') + .stub(Blockly.common.getSelected(), 'isOwnDeletable') .returns(false); this.injectionDiv.dispatchEvent(keyEvent); sinon.assert.notCalled(this.copySpy); + sinon.assert.notCalled(this.disposeSpy); sinon.assert.notCalled(this.hideChaffSpy); }); }); }); - // Do not copy a block if it is not movable. + // Do not cut a block if it is not movable. suite('Block is not movable', function () { testCases.forEach(function (testCase) { const testCaseName = testCase[0]; const keyEvent = testCase[1]; test(testCaseName, function () { - sinon.stub(Blockly.common.getSelected(), 'isMovable').returns(false); + sinon + .stub(Blockly.common.getSelected(), 'isOwnMovable') + .returns(false); this.injectionDiv.dispatchEvent(keyEvent); sinon.assert.notCalled(this.copySpy); + sinon.assert.notCalled(this.disposeSpy); sinon.assert.notCalled(this.hideChaffSpy); }); }); }); + test('Not called when connection is focused', function () { + // Restore the stub behavior called during setup + Blockly.getFocusManager().getFocusedNode.restore(); + + setSelectedConnection(this.workspace); + const event = createKeyDownEvent(Blockly.utils.KeyCodes.C, [ + Blockly.utils.KeyCodes.CTRL, + ]); + this.injectionDiv.dispatchEvent(event); + sinon.assert.notCalled(this.copySpy); + sinon.assert.notCalled(this.disposeSpy); + sinon.assert.notCalled(this.hideChaffSpy); + }); + + // Cut a comment. + suite('Workspace comment', function () { + testCases.forEach(function (testCase) { + const testCaseName = testCase[0]; + const keyEvent = testCase[1]; + test(testCaseName, function () { + Blockly.getFocusManager().getFocusedNode.restore(); + this.comment = setSelectedComment(this.workspace); + this.copySpy = sinon.spy(this.comment, 'toCopyData'); + this.disposeSpy = sinon.spy(this.comment, 'dispose'); + + this.injectionDiv.dispatchEvent(keyEvent); + sinon.assert.calledOnce(this.copySpy); + sinon.assert.calledOnce(this.disposeSpy); + }); + }); + }); + }); + + suite('Paste', function () { + test('Disabled when nothing has been copied', function () { + const pasteShortcut = + Blockly.ShortcutRegistry.registry.getRegistry()[ + Blockly.ShortcutItems.names.PASTE + ]; + Blockly.clipboard.setLastCopiedData(undefined); + + const isPasteEnabled = pasteShortcut.preconditionFn(); + assert.isFalse(isPasteEnabled); + }); }); suite('Undo', function () { @@ -222,12 +434,6 @@ suite('Key Down', function () { Blockly.utils.KeyCodes.META, ]), ], - [ - 'Alt Z', - createKeyDownEvent(Blockly.utils.KeyCodes.Z, [ - Blockly.utils.KeyCodes.ALT, - ]), - ], ]; // Undo. suite('Simple', function () { @@ -242,13 +448,13 @@ suite('Key Down', function () { }); }); }); - // Do not undo if a gesture is in progress. - suite('Gesture in progress', function () { + // Do not undo if a drag is in progress. + suite('Drag in progress', function () { testCases.forEach(function (testCase) { const testCaseName = testCase[0]; const keyEvent = testCase[1]; test(testCaseName, function () { - sinon.stub(Blockly.Gesture, 'inProgress').returns(true); + sinon.stub(this.workspace, 'isDragging').returns(true); this.injectionDiv.dispatchEvent(keyEvent); sinon.assert.notCalled(this.undoSpy); sinon.assert.notCalled(this.hideChaffSpy); @@ -288,13 +494,6 @@ suite('Key Down', function () { Blockly.utils.KeyCodes.SHIFT, ]), ], - [ - 'Alt Shift Z', - createKeyDownEvent(Blockly.utils.KeyCodes.Z, [ - Blockly.utils.KeyCodes.ALT, - Blockly.utils.KeyCodes.SHIFT, - ]), - ], ]; // Undo. suite('Simple', function () { @@ -309,13 +508,13 @@ suite('Key Down', function () { }); }); }); - // Do not undo if a gesture is in progress. - suite('Gesture in progress', function () { + // Do not redo if a drag is in progress. + suite('Drag in progress', function () { testCases.forEach(function (testCase) { const testCaseName = testCase[0]; const keyEvent = testCase[1]; test(testCaseName, function () { - sinon.stub(Blockly.Gesture, 'inProgress').returns(true); + sinon.stub(this.workspace, 'isDragging').returns(true); this.injectionDiv.dispatchEvent(keyEvent); sinon.assert.notCalled(this.redoSpy); sinon.assert.notCalled(this.hideChaffSpy); @@ -349,8 +548,8 @@ suite('Key Down', function () { sinon.assert.calledWith(this.undoSpy, true); sinon.assert.calledOnce(this.hideChaffSpy); }); - test('Not called when a gesture is in progress', function () { - sinon.stub(Blockly.Gesture, 'inProgress').returns(true); + test('Not called when a drag is in progress', function () { + sinon.stub(this.workspace, 'isDragging').returns(true); this.injectionDiv.dispatchEvent(this.ctrlYEvent); sinon.assert.notCalled(this.undoSpy); sinon.assert.notCalled(this.hideChaffSpy); diff --git a/tests/mocha/shortcut_registry_test.js b/packages/blockly/tests/mocha/shortcut_registry_test.js similarity index 84% rename from tests/mocha/shortcut_registry_test.js rename to packages/blockly/tests/mocha/shortcut_registry_test.js index 37c1b9c2023..a06f01b9c00 100644 --- a/tests/mocha/shortcut_registry_test.js +++ b/packages/blockly/tests/mocha/shortcut_registry_test.js @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; +import {createTestBlock} from './test_helpers/block_definitions.js'; import { sharedTestSetup, sharedTestTeardown, @@ -259,7 +260,7 @@ suite('Keyboard Shortcut Registry Test', function () { assert.equal(this.registry.getKeyMap()['keyCode'][0], 'a'); }); test('Gets a copy of the registry', function () { - const shortcut = {'name': 'shortcutName'}; + const shortcut = {'name': 'shortcutName', 'keyCodes': ['2', '4']}; this.registry.register(shortcut); const registrycopy = this.registry.getRegistry(); registrycopy['shortcutName']['name'] = 'shortcutName1'; @@ -267,6 +268,10 @@ suite('Keyboard Shortcut Registry Test', function () { this.registry.getRegistry()['shortcutName']['name'], 'shortcutName', ); + assert.deepEqual( + this.registry.getRegistry()['shortcutName']['keyCodes'], + shortcut['keyCodes'], + ); }); test('Gets keyboard shortcuts from a key code', function () { this.registry.setKeyMap({'keyCode': ['shortcutName']}); @@ -299,7 +304,7 @@ suite('Keyboard Shortcut Registry Test', function () { 'callback': function () { return true; }, - 'precondition': function () { + 'preconditionFn': function () { return true; }, }; @@ -319,6 +324,27 @@ suite('Keyboard Shortcut Registry Test', function () { const event = createKeyDownEvent(Blockly.utils.KeyCodes.D); assert.isFalse(this.registry.onKeyDown(this.workspace, event)); }); + test('No callback if precondition fails', function () { + const shortcut = { + 'name': 'test_shortcut', + 'callback': function () { + return true; + }, + 'preconditionFn': function () { + return false; + }, + }; + const callBackStub = addShortcut( + this.registry, + shortcut, + Blockly.utils.KeyCodes.C, + true, + ); + const event = createKeyDownEvent(Blockly.utils.KeyCodes.C); + assert.isFalse(this.registry.onKeyDown(this.workspace, event)); + sinon.assert.notCalled(callBackStub); + }); + test('No precondition available - execute callback', function () { delete this.testShortcut['precondition']; const event = createKeyDownEvent(Blockly.utils.KeyCodes.C); @@ -332,8 +358,8 @@ suite('Keyboard Shortcut Registry Test', function () { 'callback': function () { return false; }, - 'precondition': function () { - return false; + 'preconditionFn': function () { + return true; }, }; const testShortcut2Stub = addShortcut( @@ -353,8 +379,8 @@ suite('Keyboard Shortcut Registry Test', function () { 'callback': function () { return false; }, - 'precondition': function () { - return false; + 'preconditionFn': function () { + return true; }, }; const testShortcut2Stub = addShortcut( @@ -367,6 +393,63 @@ suite('Keyboard Shortcut Registry Test', function () { sinon.assert.calledOnce(testShortcut2Stub); sinon.assert.notCalled(this.callBackStub); }); + suite('interaction with FocusManager', function () { + setup(function () { + this.testShortcutWithScope = { + 'name': 'test_shortcut', + 'callback': function (workspace, e, shortcut, scope) { + return true; + }, + 'preconditionFn': function (workspace, scope) { + return true; + }, + }; + + // Stub the focus manager + this.focusedBlock = createTestBlock(); + sinon + .stub(Blockly.getFocusManager(), 'getFocusedNode') + .returns(this.focusedBlock); + }); + test('Callback receives the focused node', function () { + const event = createKeyDownEvent(Blockly.utils.KeyCodes.C); + const callbackStub = addShortcut( + this.registry, + this.testShortcutWithScope, + Blockly.utils.KeyCodes.C, + true, + ); + this.registry.onKeyDown(this.workspace, event); + + const expectedScope = {focusedNode: this.focusedBlock}; + sinon.assert.calledWithExactly( + callbackStub, + this.workspace, + event, + this.testShortcutWithScope, + expectedScope, + ); + }); + test('Precondition receives the focused node', function () { + const event = createKeyDownEvent(Blockly.utils.KeyCodes.C); + const callbackStub = addShortcut( + this.registry, + this.testShortcutWithScope, + Blockly.utils.KeyCodes.C, + true, + ); + const preconditionStub = sinon + .stub(this.testShortcutWithScope, 'preconditionFn') + .returns(true); + this.registry.onKeyDown(this.workspace, event); + const expectedScope = {focusedNode: this.focusedBlock}; + sinon.assert.calledWithExactly( + preconditionStub, + this.workspace, + expectedScope, + ); + }); + }); }); suite('createSerializedKey', function () { diff --git a/tests/mocha/test_helpers/block_definitions.js b/packages/blockly/tests/mocha/test_helpers/block_definitions.js similarity index 100% rename from tests/mocha/test_helpers/block_definitions.js rename to packages/blockly/tests/mocha/test_helpers/block_definitions.js diff --git a/tests/mocha/test_helpers/code_generation.js b/packages/blockly/tests/mocha/test_helpers/code_generation.js similarity index 98% rename from tests/mocha/test_helpers/code_generation.js rename to packages/blockly/tests/mocha/test_helpers/code_generation.js index 95bd902cd45..e61a45653b2 100644 --- a/tests/mocha/test_helpers/code_generation.js +++ b/packages/blockly/tests/mocha/test_helpers/code_generation.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../../node_modules/chai/chai.js'; +import {assert} from '../../../node_modules/chai/index.js'; import {runTestSuites} from './common.js'; /** diff --git a/tests/mocha/test_helpers/common.js b/packages/blockly/tests/mocha/test_helpers/common.js similarity index 100% rename from tests/mocha/test_helpers/common.js rename to packages/blockly/tests/mocha/test_helpers/common.js diff --git a/tests/mocha/test_helpers/events.js b/packages/blockly/tests/mocha/test_helpers/events.js similarity index 99% rename from tests/mocha/test_helpers/events.js rename to packages/blockly/tests/mocha/test_helpers/events.js index c074bdd77a4..3e0b1e95d8b 100644 --- a/tests/mocha/test_helpers/events.js +++ b/packages/blockly/tests/mocha/test_helpers/events.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../../node_modules/chai/chai.js'; +import {assert} from '../../../node_modules/chai/index.js'; /** * Creates spy for workspace fireChangeListener diff --git a/tests/mocha/test_helpers/fields.js b/packages/blockly/tests/mocha/test_helpers/fields.js similarity index 99% rename from tests/mocha/test_helpers/fields.js rename to packages/blockly/tests/mocha/test_helpers/fields.js index e082abb4ccc..ab304a808a7 100644 --- a/tests/mocha/test_helpers/fields.js +++ b/packages/blockly/tests/mocha/test_helpers/fields.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../../node_modules/chai/chai.js'; +import {assert} from '../../../node_modules/chai/index.js'; import {runTestCases, TestCase} from './common.js'; /** diff --git a/packages/blockly/tests/mocha/test_helpers/icon_mocks.js b/packages/blockly/tests/mocha/test_helpers/icon_mocks.js new file mode 100644 index 00000000000..0e549b9764c --- /dev/null +++ b/packages/blockly/tests/mocha/test_helpers/icon_mocks.js @@ -0,0 +1,130 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {isFocusableNode} from '../../../build/src/core/interfaces/i_focusable_node.js'; +import {hasBubble} from '../../../build/src/core/interfaces/i_has_bubble.js'; +import {isIcon} from '../../../build/src/core/interfaces/i_icon.js'; +import {isSerializable} from '../../../build/src/core/interfaces/i_serializable.js'; + +export class MockFocusable { + getFocusableElement() {} + getFocusableTree() {} + onNodeFocus() {} + onNodeBlur() {} + canBeFocused() {} +} + +if (!isFocusableNode(new MockFocusable())) { + throw new TypeError('MockFocusable not an IFocuableNode'); +} + +export class MockIcon extends MockFocusable { + getType() { + return new Blockly.icons.IconType('mock icon'); + } + + initView() {} + + dispose() {} + + getWeight() {} + + getSize() { + return new Blockly.utils.Size(0, 0); + } + + applyColour() {} + + hideForInsertionMarker() {} + + updateEditable() {} + + updateCollapsed() {} + + isShownWhenCollapsed() {} + + setOffsetInBlock() {} + + onLocationChange() {} + + onClick() {} + + getFocusableElement() { + throw new Error('Unsupported operation in mock.'); + } + + getFocusableTree() { + throw new Error('Unsupported operation in mock.'); + } + + onNodeFocus() {} + + onNodeBlur() {} + + canBeFocused() { + return false; + } +} + +if (!isIcon(new MockIcon())) { + throw new TypeError('MockIcon not an IIcon'); +} + +export class MockSerializableIcon extends MockIcon { + constructor() { + super(); + this.state = ''; + } + + getType() { + return new Blockly.icons.IconType('serializable icon'); + } + + getWeight() { + return 1; + } + + saveState() { + return 'some state'; + } + + loadState(state) { + this.state = state; + } +} + +if (!isSerializable(new MockSerializableIcon())) { + throw new TypeError('MockSerializableIcon not an ISerializable'); +} + +export class MockBubbleIcon extends MockIcon { + constructor() { + super(); + this.visible = false; + } + + getType() { + return new Blockly.icons.IconType('bubble icon'); + } + + updateCollapsed() {} + + bubbleIsVisible() { + return this.visible; + } + + setBubbleVisible(visible) { + this.visible = visible; + } + + getBubble() { + return null; + } +} + +if (!hasBubble(new MockBubbleIcon())) { + throw new TypeError('MockBubbleIcon not an IHasBubble'); +} diff --git a/tests/mocha/test_helpers/procedures.js b/packages/blockly/tests/mocha/test_helpers/procedures.js similarity index 97% rename from tests/mocha/test_helpers/procedures.js rename to packages/blockly/tests/mocha/test_helpers/procedures.js index ecf8c13adcc..2ea54a6c204 100644 --- a/tests/mocha/test_helpers/procedures.js +++ b/packages/blockly/tests/mocha/test_helpers/procedures.js @@ -6,7 +6,7 @@ import {ConnectionType} from '../../../build/src/core/connection_type.js'; import {VariableModel} from '../../../build/src/core/variable_model.js'; -import {assert} from '../../../node_modules/chai/chai.js'; +import {assert} from '../../../node_modules/chai/index.js'; /** * Asserts that the procedure definition or call block has the expected var @@ -18,7 +18,9 @@ import {assert} from '../../../node_modules/chai/chai.js'; function assertBlockVarModels(block, varIds) { const expectedVarModels = []; for (let i = 0; i < varIds.length; i++) { - expectedVarModels.push(block.workspace.getVariableById(varIds[i])); + expectedVarModels.push( + block.workspace.getVariableMap().getVariableById(varIds[i]), + ); } assert.sameDeepOrderedMembers(block.getVarModels(), expectedVarModels); } diff --git a/tests/mocha/test_helpers/serialization.js b/packages/blockly/tests/mocha/test_helpers/serialization.js similarity index 98% rename from tests/mocha/test_helpers/serialization.js rename to packages/blockly/tests/mocha/test_helpers/serialization.js index c99f508d4dc..c476eae3d6f 100644 --- a/tests/mocha/test_helpers/serialization.js +++ b/packages/blockly/tests/mocha/test_helpers/serialization.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../../node_modules/chai/chai.js'; +import {assert} from '../../../node_modules/chai/index.js'; import {runTestCases} from './common.js'; /** diff --git a/tests/mocha/test_helpers/setup_teardown.js b/packages/blockly/tests/mocha/test_helpers/setup_teardown.js similarity index 87% rename from tests/mocha/test_helpers/setup_teardown.js rename to packages/blockly/tests/mocha/test_helpers/setup_teardown.js index 2fc08cb6943..b0d7c83c697 100644 --- a/tests/mocha/test_helpers/setup_teardown.js +++ b/packages/blockly/tests/mocha/test_helpers/setup_teardown.js @@ -5,6 +5,7 @@ */ import * as eventUtils from '../../../build/src/core/events/utils.js'; +import {FocusManager} from '../../../build/src/core/focus_manager.js'; /** * Safely disposes of Blockly workspace, logging any errors. @@ -124,6 +125,18 @@ export function sharedTestSetup(options = {}) { }; this.blockTypesCleanup_ = this.sharedCleanup.blockTypesCleanup_; this.messagesCleanup_ = this.sharedCleanup.messagesCleanup_; + + // Set up FocusManager to run in isolation for this test. + this.globalDocumentEventListeners = []; + const testState = this; + const addDocumentEventListener = function (type, listener) { + testState.globalDocumentEventListeners.push({type, listener}); + document.addEventListener(type, listener); + }; + const specificFocusManager = new FocusManager(addDocumentEventListener); + this.oldGetFocusManager = FocusManager.getFocusManager; + FocusManager.getFocusManager = () => specificFocusManager; + wrapDefineBlocksWithJsonArrayWithCleanup_(this.sharedCleanup); return { clock: this.clock, @@ -184,6 +197,16 @@ export function sharedTestTeardown() { } Blockly.WidgetDiv.testOnly_setDiv(null); + + // Remove the globally registered listener from FocusManager to avoid state + // being shared across test boundaries. + for (const registeredListener of this.globalDocumentEventListeners) { + const eventType = registeredListener.type; + const eventListener = registeredListener.listener; + document.removeEventListener(eventType, eventListener); + } + this.globalDocumentEventListeners = []; + FocusManager.getFocusManager = this.oldGetFocusManager; } } diff --git a/tests/mocha/test_helpers/toolbox_definitions.js b/packages/blockly/tests/mocha/test_helpers/toolbox_definitions.js similarity index 96% rename from tests/mocha/test_helpers/toolbox_definitions.js rename to packages/blockly/tests/mocha/test_helpers/toolbox_definitions.js index 427331bcf01..42de6cf2ec7 100644 --- a/tests/mocha/test_helpers/toolbox_definitions.js +++ b/packages/blockly/tests/mocha/test_helpers/toolbox_definitions.js @@ -243,9 +243,8 @@ export function getBasicToolbox() { } export function getCollapsibleItem(toolbox) { - const contents = toolbox.contents_; - for (let i = 0; i < contents.length; i++) { - const item = contents[i]; + const contents = toolbox.contents.values(); + for (const item of contents) { if (item.isCollapsible()) { return item; } @@ -253,9 +252,8 @@ export function getCollapsibleItem(toolbox) { } export function getNonCollapsibleItem(toolbox) { - const contents = toolbox.contents_; - for (let i = 0; i < contents.length; i++) { - const item = contents[i]; + const contents = toolbox.contents.values(); + for (const item of contents) { if (!item.isCollapsible()) { return item; } diff --git a/tests/mocha/test_helpers/user_input.js b/packages/blockly/tests/mocha/test_helpers/user_input.js similarity index 100% rename from tests/mocha/test_helpers/user_input.js rename to packages/blockly/tests/mocha/test_helpers/user_input.js diff --git a/tests/mocha/test_helpers/variables.js b/packages/blockly/tests/mocha/test_helpers/variables.js similarity index 74% rename from tests/mocha/test_helpers/variables.js rename to packages/blockly/tests/mocha/test_helpers/variables.js index 83f175f6327..3c6ab9332ae 100644 --- a/tests/mocha/test_helpers/variables.js +++ b/packages/blockly/tests/mocha/test_helpers/variables.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../../node_modules/chai/chai.js'; +import {assert} from '../../../node_modules/chai/index.js'; /** * Check if a variable with the given values exists. @@ -15,7 +15,11 @@ import {assert} from '../../../node_modules/chai/chai.js'; * @param {!string} id The expected id of the variable. */ export function assertVariableValues(container, name, type, id) { - const variable = container.getVariableById(id); + const variableMap = + container instanceof Blockly.Workspace + ? container.getVariableMap() + : container; + const variable = variableMap.getVariableById(id); assert.isDefined(variable); assert.equal(variable.name, name); assert.equal(variable.type, type); diff --git a/tests/mocha/test_helpers/warnings.js b/packages/blockly/tests/mocha/test_helpers/warnings.js similarity index 97% rename from tests/mocha/test_helpers/warnings.js rename to packages/blockly/tests/mocha/test_helpers/warnings.js index 0e07f846c5a..d718a25c166 100644 --- a/tests/mocha/test_helpers/warnings.js +++ b/packages/blockly/tests/mocha/test_helpers/warnings.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../../node_modules/chai/chai.js'; +import {assert} from '../../../node_modules/chai/index.js'; /** * Captures the strings sent to console.warn() when calling a function. diff --git a/tests/mocha/test_helpers/workspace.js b/packages/blockly/tests/mocha/test_helpers/workspace.js similarity index 80% rename from tests/mocha/test_helpers/workspace.js rename to packages/blockly/tests/mocha/test_helpers/workspace.js index 40b2574fca1..9a1633da561 100644 --- a/tests/mocha/test_helpers/workspace.js +++ b/packages/blockly/tests/mocha/test_helpers/workspace.js @@ -5,10 +5,9 @@ */ import * as eventUtils from '../../../build/src/core/events/utils.js'; -import {assert} from '../../../node_modules/chai/chai.js'; +import {assert} from '../../../node_modules/chai/index.js'; import {workspaceTeardown} from './setup_teardown.js'; import {assertVariableValues} from './variables.js'; -import {assertWarnings} from './warnings.js'; export function testAWorkspace() { setup(function () { @@ -25,6 +24,7 @@ export function testAWorkspace() { ], }, ]); + this.variableMap = this.workspace.getVariableMap(); }); teardown(function () { @@ -68,13 +68,13 @@ export function testAWorkspace() { suite('clear', function () { test('Trivial', function () { sinon.stub(eventUtils.TEST_ONLY, 'setGroupInternal').returns(null); - this.workspace.createVariable('name1', 'type1', 'id1'); - this.workspace.createVariable('name2', 'type2', 'id2'); + this.variableMap.createVariable('name1', 'type1', 'id1'); + this.variableMap.createVariable('name2', 'type2', 'id2'); this.workspace.newBlock(''); this.workspace.clear(); assert.equal(this.workspace.getTopBlocks(false).length, 0); - const varMapLength = this.workspace.getVariableMap().variableMap.size; + const varMapLength = this.variableMap.variableMap.size; assert.equal(varMapLength, 0); }); @@ -84,7 +84,7 @@ export function testAWorkspace() { this.workspace.clear(); assert.equal(this.workspace.getTopBlocks(false).length, 0); - const varMapLength = this.workspace.getVariableMap().variableMap.size; + const varMapLength = this.variableMap.variableMap.size; assert.equal(varMapLength, 0); }); }); @@ -92,102 +92,109 @@ export function testAWorkspace() { suite('deleteVariable', function () { setup(function () { // Create two variables of different types. - this.var1 = this.workspace.createVariable('name1', 'type1', 'id1'); - this.var2 = this.workspace.createVariable('name2', 'type2', 'id2'); + this.var1 = this.variableMap.createVariable('name1', 'type1', 'id1'); + this.var2 = this.variableMap.createVariable('name2', 'type2', 'id2'); // Create blocks to refer to both of them. createVarBlocksNoEvents(this.workspace, ['id1', 'id1', 'id2']); }); test('deleteVariableById(id2) one usage', function () { // Deleting variable one usage should not trigger confirm dialog. - const stub = sinon - .stub(Blockly.dialog.TEST_ONLY, 'confirmInternal') - .callsArgWith(1, true); - this.workspace.deleteVariableById('id2'); + const stub = sinon.stub(window, 'confirm').returns(true); + const id2 = this.variableMap.getVariableById('id2'); + Blockly.Variables.deleteVariable(this.workspace, id2); sinon.assert.notCalled(stub); - const variable = this.workspace.getVariableById('id2'); + const variable = this.variableMap.getVariableById('id2'); assert.isNull(variable); - assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); + assertVariableValues(this.variableMap, 'name1', 'type1', 'id1'); assertBlockVarModelName(this.workspace, 0, 'name1'); + + stub.restore(); }); test('deleteVariableById(id1) multiple usages confirm', function () { // Deleting variable with multiple usages triggers confirm dialog. - const stub = sinon - .stub(Blockly.dialog.TEST_ONLY, 'confirmInternal') - .callsArgWith(1, true); - this.workspace.deleteVariableById('id1'); + const stub = sinon.stub(window, 'confirm').returns(true); + const id1 = this.variableMap.getVariableById('id1'); + Blockly.Variables.deleteVariable(this.workspace, id1); sinon.assert.calledOnce(stub); - const variable = this.workspace.getVariableById('id1'); + const variable = this.variableMap.getVariableById('id1'); assert.isNull(variable); - assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); + assertVariableValues(this.variableMap, 'name2', 'type2', 'id2'); assertBlockVarModelName(this.workspace, 0, 'name2'); + + stub.restore(); }); test('deleteVariableById(id1) multiple usages cancel', function () { // Deleting variable with multiple usages triggers confirm dialog. - const stub = sinon - .stub(Blockly.dialog.TEST_ONLY, 'confirmInternal') - .callsArgWith(1, false); - this.workspace.deleteVariableById('id1'); + const stub = sinon.stub(window, 'confirm').returns(false); + const id1 = this.variableMap.getVariableById('id1'); + Blockly.Variables.deleteVariable(this.workspace, id1); sinon.assert.calledOnce(stub); - assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); - assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); + assertVariableValues(this.variableMap, 'name1', 'type1', 'id1'); + assertVariableValues(this.variableMap, 'name2', 'type2', 'id2'); assertBlockVarModelName(this.workspace, 0, 'name1'); assertBlockVarModelName(this.workspace, 1, 'name1'); assertBlockVarModelName(this.workspace, 2, 'name2'); + + stub.restore(); }); }); - suite('renameVariableById', function () { + suite('renameVariable', function () { setup(function () { - this.workspace.createVariable('name1', 'type1', 'id1'); + this.variableMap.createVariable('name1', 'type1', 'id1'); }); test('No references rename to name2', function () { - this.workspace.renameVariableById('id1', 'name2'); - assertVariableValues(this.workspace, 'name2', 'type1', 'id1'); + const id1 = this.variableMap.getVariableById('id1'); + this.variableMap.renameVariable(id1, 'name2'); + assertVariableValues(this.variableMap, 'name2', 'type1', 'id1'); // Renaming should not have created a new variable. - assert.equal(this.workspace.getAllVariables().length, 1); + assert.equal(this.variableMap.getAllVariables().length, 1); }); test('Reference exists rename to name2', function () { createVarBlocksNoEvents(this.workspace, ['id1']); - this.workspace.renameVariableById('id1', 'name2'); - assertVariableValues(this.workspace, 'name2', 'type1', 'id1'); + const id1 = this.variableMap.getVariableById('id1'); + this.variableMap.renameVariable(id1, 'name2'); + assertVariableValues(this.variableMap, 'name2', 'type1', 'id1'); // Renaming should not have created a new variable. - assert.equal(this.workspace.getAllVariables().length, 1); + assert.equal(this.variableMap.getAllVariables().length, 1); assertBlockVarModelName(this.workspace, 0, 'name2'); }); test('Reference exists different capitalization rename to Name1', function () { createVarBlocksNoEvents(this.workspace, ['id1']); - this.workspace.renameVariableById('id1', 'Name1'); - assertVariableValues(this.workspace, 'Name1', 'type1', 'id1'); + const id1 = this.variableMap.getVariableById('id1'); + this.variableMap.renameVariable(id1, 'Name1'); + assertVariableValues(this.variableMap, 'Name1', 'type1', 'id1'); // Renaming should not have created a new variable. - assert.equal(this.workspace.getAllVariables().length, 1); + assert.equal(this.variableMap.getAllVariables().length, 1); assertBlockVarModelName(this.workspace, 0, 'Name1'); }); suite('Two variables rename overlap', function () { test('Same type rename variable with id1 to name2', function () { - this.workspace.createVariable('name2', 'type1', 'id2'); + this.variableMap.createVariable('name2', 'type1', 'id2'); createVarBlocksNoEvents(this.workspace, ['id1', 'id2']); - this.workspace.renameVariableById('id1', 'name2'); + const id1 = this.variableMap.getVariableById('id1'); + this.variableMap.renameVariable(id1, 'name2'); // The second variable should remain unchanged. assertVariableValues(this.workspace, 'name2', 'type1', 'id2'); // The first variable should have been deleted. - const variable = this.workspace.getVariableById('id1'); + const variable = this.variableMap.getVariableById('id1'); assert.isNull(variable); // There should only be one variable left. - assert.equal(this.workspace.getAllVariables().length, 1); + assert.equal(this.variableMap.getAllVariables().length, 1); // Both blocks should now reference variable with name2. assertBlockVarModelName(this.workspace, 0, 'name2'); @@ -195,14 +202,15 @@ export function testAWorkspace() { }); test('Different type rename variable with id1 to name2', function () { - this.workspace.createVariable('name2', 'type2', 'id2'); + this.variableMap.createVariable('name2', 'type2', 'id2'); createVarBlocksNoEvents(this.workspace, ['id1', 'id2']); - this.workspace.renameVariableById('id1', 'name2'); + const id1 = this.variableMap.getVariableById('id1'); + this.variableMap.renameVariable(id1, 'name2'); // Variables with different type are allowed to have the same name. - assertVariableValues(this.workspace, 'name2', 'type1', 'id1'); - assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); + assertVariableValues(this.variableMap, 'name2', 'type1', 'id1'); + assertVariableValues(this.variableMap, 'name2', 'type2', 'id2'); // Both blocks should now reference variable with name2. assertBlockVarModelName(this.workspace, 0, 'name2'); @@ -210,18 +218,19 @@ export function testAWorkspace() { }); test('Same type different capitalization rename variable with id1 to Name2', function () { - this.workspace.createVariable('name2', 'type1', 'id2'); + this.variableMap.createVariable('name2', 'type1', 'id2'); createVarBlocksNoEvents(this.workspace, ['id1', 'id2']); - this.workspace.renameVariableById('id1', 'Name2'); + const id1 = this.variableMap.getVariableById('id1'); + this.variableMap.renameVariable(id1, 'Name2'); // The second variable should be updated. - assertVariableValues(this.workspace, 'Name2', 'type1', 'id2'); + assertVariableValues(this.variableMap, 'Name2', 'type1', 'id2'); // The first variable should have been deleted. - const variable = this.workspace.getVariableById('id1'); + const variable = this.variableMap.getVariableById('id1'); assert.isNull(variable); // There should only be one variable left. - assert.equal(this.workspace.getAllVariables().length, 1); + assert.equal(this.variableMap.getAllVariables().length, 1); // Both blocks should now reference variable with Name2. assertBlockVarModelName(this.workspace, 0, 'Name2'); @@ -229,15 +238,16 @@ export function testAWorkspace() { }); test('Different type different capitalization rename variable with id1 to Name2', function () { - this.workspace.createVariable('name2', 'type2', 'id2'); + this.variableMap.createVariable('name2', 'type2', 'id2'); createVarBlocksNoEvents(this.workspace, ['id1', 'id2']); - this.workspace.renameVariableById('id1', 'Name2'); + const id1 = this.variableMap.getVariableById('id1'); + this.variableMap.renameVariable(id1, 'Name2'); // Variables with different type are allowed to have the same name. - assertVariableValues(this.workspace, 'Name2', 'type1', 'id1'); + assertVariableValues(this.variableMap, 'Name2', 'type1', 'id1'); // Second variable should remain unchanged. - assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); + assertVariableValues(this.variableMap, 'name2', 'type2', 'id2'); // Only first block should use new capitalization. assertBlockVarModelName(this.workspace, 0, 'Name2'); @@ -1249,8 +1259,8 @@ export function testAWorkspace() { suite('Variables', function () { function createTwoVarsDifferentTypes(workspace) { - workspace.createVariable('name1', 'type1', 'id1'); - workspace.createVariable('name2', 'type2', 'id2'); + workspace.getVariableMap().createVariable('name1', 'type1', 'id1'); + workspace.getVariableMap().createVariable('name2', 'type2', 'id2'); } suite('createVariable', function () { @@ -1261,11 +1271,11 @@ export function testAWorkspace() { this.workspace.undo(); this.clock.runAll(); assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); - assert.isNull(this.workspace.getVariableById('id2')); + assert.isNull(this.variableMap.getVariableById('id2')); this.workspace.undo(); - assert.isNull(this.workspace.getVariableById('id1')); - assert.isNull(this.workspace.getVariableById('id2')); + assert.isNull(this.variableMap.getVariableById('id1')); + assert.isNull(this.variableMap.getVariableById('id2')); }); test('Undo and redo', function () { @@ -1275,7 +1285,7 @@ export function testAWorkspace() { this.workspace.undo(); this.clock.runAll(); assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); - assert.isNull(this.workspace.getVariableById('id2')); + assert.isNull(this.variableMap.getVariableById('id2')); this.workspace.undo(true); @@ -1285,13 +1295,13 @@ export function testAWorkspace() { this.workspace.undo(); this.workspace.undo(); - assert.isNull(this.workspace.getVariableById('id1')); - assert.isNull(this.workspace.getVariableById('id2')); + assert.isNull(this.variableMap.getVariableById('id1')); + assert.isNull(this.variableMap.getVariableById('id2')); this.workspace.undo(true); // Expect that variable 'id1' is recreated assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); - assert.isNull(this.workspace.getVariableById('id2')); + assert.isNull(this.variableMap.getVariableById('id2')); }); }); @@ -1299,13 +1309,15 @@ export function testAWorkspace() { test('Undo only no usages', function () { createTwoVarsDifferentTypes(this.workspace); this.clock.runAll(); - this.workspace.deleteVariableById('id1'); - this.workspace.deleteVariableById('id2'); + const id1 = this.variableMap.getVariableById('id1'); + Blockly.Variables.deleteVariable(this.workspace, id1); + const id2 = this.variableMap.getVariableById('id2'); + Blockly.Variables.deleteVariable(this.workspace, id2); this.clock.runAll(); this.workspace.undo(); this.clock.runAll(); - assert.isNull(this.workspace.getVariableById('id1')); + assert.isNull(this.variableMap.getVariableById('id1')); assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); this.workspace.undo(); @@ -1319,14 +1331,16 @@ export function testAWorkspace() { // Create blocks to refer to both of them. createVarBlocksNoEvents(this.workspace, ['id1', 'id2']); this.clock.runAll(); - this.workspace.deleteVariableById('id1'); - this.workspace.deleteVariableById('id2'); + const id1 = this.variableMap.getVariableById('id1'); + Blockly.Variables.deleteVariable(this.workspace, id1); + const id2 = this.variableMap.getVariableById('id2'); + Blockly.Variables.deleteVariable(this.workspace, id2); this.clock.runAll(); this.workspace.undo(); this.clock.runAll(); assertBlockVarModelName(this.workspace, 0, 'name2'); - assert.isNull(this.workspace.getVariableById('id1')); + assert.isNull(this.variableMap.getVariableById('id1')); assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); this.workspace.undo(); @@ -1340,20 +1354,22 @@ export function testAWorkspace() { test('Reference exists no usages', function () { createTwoVarsDifferentTypes(this.workspace); this.clock.runAll(); - this.workspace.deleteVariableById('id1'); - this.workspace.deleteVariableById('id2'); + const id1 = this.variableMap.getVariableById('id1'); + Blockly.Variables.deleteVariable(this.workspace, id1); + const id2 = this.variableMap.getVariableById('id2'); + Blockly.Variables.deleteVariable(this.workspace, id2); this.clock.runAll(); this.workspace.undo(); this.clock.runAll(); - assert.isNull(this.workspace.getVariableById('id1')); + assert.isNull(this.variableMap.getVariableById('id1')); assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); this.workspace.undo(true); this.clock.runAll(); // Expect that both variables are deleted - assert.isNull(this.workspace.getVariableById('id1')); - assert.isNull(this.workspace.getVariableById('id2')); + assert.isNull(this.variableMap.getVariableById('id1')); + assert.isNull(this.variableMap.getVariableById('id2')); this.workspace.undo(); this.clock.runAll(); @@ -1365,7 +1381,7 @@ export function testAWorkspace() { this.workspace.undo(true); this.clock.runAll(); // Expect that variable 'id2' is recreated - assert.isNull(this.workspace.getVariableById('id1')); + assert.isNull(this.variableMap.getVariableById('id1')); assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); }); @@ -1374,22 +1390,24 @@ export function testAWorkspace() { // Create blocks to refer to both of them. createVarBlocksNoEvents(this.workspace, ['id1', 'id2']); this.clock.runAll(); - this.workspace.deleteVariableById('id1'); - this.workspace.deleteVariableById('id2'); + const id1 = this.variableMap.getVariableById('id1'); + Blockly.Variables.deleteVariable(this.workspace, id1); + const id2 = this.variableMap.getVariableById('id2'); + Blockly.Variables.deleteVariable(this.workspace, id2); this.clock.runAll(); this.workspace.undo(); this.clock.runAll(); assertBlockVarModelName(this.workspace, 0, 'name2'); - assert.isNull(this.workspace.getVariableById('id1')); + assert.isNull(this.variableMap.getVariableById('id1')); assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); this.workspace.undo(true); this.clock.runAll(); // Expect that both variables are deleted assert.equal(this.workspace.getTopBlocks(false).length, 0); - assert.isNull(this.workspace.getVariableById('id1')); - assert.isNull(this.workspace.getVariableById('id2')); + assert.isNull(this.variableMap.getVariableById('id1')); + assert.isNull(this.variableMap.getVariableById('id2')); this.workspace.undo(); this.clock.runAll(); @@ -1404,253 +1422,202 @@ export function testAWorkspace() { this.clock.runAll(); // Expect that variable 'id2' is recreated assertBlockVarModelName(this.workspace, 0, 'name2'); - assert.isNull(this.workspace.getVariableById('id1')); + assert.isNull(this.variableMap.getVariableById('id1')); assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); }); - - test('Delete same variable twice no usages', function () { - this.workspace.createVariable('name1', 'type1', 'id1'); - this.workspace.deleteVariableById('id1'); - this.clock.runAll(); - const workspace = this.workspace; - assertWarnings(() => { - workspace.deleteVariableById('id1'); - }, /Can't delete/); - - // Check the undoStack only recorded one delete event. - const undoStack = this.workspace.undoStack_; - assert.equal(undoStack[undoStack.length - 1].type, 'var_delete'); - assert.notEqual(undoStack[undoStack.length - 2].type, 'var_delete'); - - // Undo delete - this.workspace.undo(); - this.clock.runAll(); - assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); - - // Redo delete - this.workspace.undo(true); - this.clock.runAll(); - assert.isNull(this.workspace.getVariableById('id1')); - - // Redo delete, nothing should happen - this.workspace.undo(true); - this.clock.runAll(); - assert.isNull(this.workspace.getVariableById('id1')); - }); - - test('Delete same variable twice with usages', function () { - this.workspace.createVariable('name1', 'type1', 'id1'); - createVarBlocksNoEvents(this.workspace, ['id1']); - this.clock.runAll(); - this.workspace.deleteVariableById('id1'); - this.clock.runAll(); - const workspace = this.workspace; - assertWarnings(() => { - workspace.deleteVariableById('id1'); - }, /Can't delete/); - - // Check the undoStack only recorded one delete event. - const undoStack = this.workspace.undoStack_; - assert.equal(undoStack[undoStack.length - 1].type, 'var_delete'); - assert.equal(undoStack[undoStack.length - 2].type, 'delete'); - assert.notEqual(undoStack[undoStack.length - 3].type, 'var_delete'); - - // Undo delete - this.workspace.undo(); - this.clock.runAll(); - assertBlockVarModelName(this.workspace, 0, 'name1'); - assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); - - // Redo delete - this.workspace.undo(true); - this.clock.runAll(); - assert.equal(this.workspace.getTopBlocks(false).length, 0); - assert.isNull(this.workspace.getVariableById('id1')); - - // Redo delete, nothing should happen - this.workspace.undo(true); - this.clock.runAll(); - assert.equal(this.workspace.getTopBlocks(false).length, 0); - assert.isNull(this.workspace.getVariableById('id1')); - }); }); - suite('renameVariableById', function () { + suite('renameVariable', function () { setup(function () { - this.workspace.createVariable('name1', 'type1', 'id1'); + this.variableMap.createVariable('name1', 'type1', 'id1'); }); test('Reference exists no usages rename to name2', function () { - this.workspace.renameVariableById('id1', 'name2'); + const id1 = this.variableMap.getVariableById('id1'); + this.variableMap.renameVariable(id1, 'name2'); this.clock.runAll(); this.workspace.undo(); this.clock.runAll(); - assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); + assertVariableValues(this.variableMap, 'name1', 'type1', 'id1'); this.workspace.undo(true); this.clock.runAll(); - assertVariableValues(this.workspace, 'name2', 'type1', 'id1'); + assertVariableValues(this.variableMap, 'name2', 'type1', 'id1'); }); test('Reference exists with usages rename to name2', function () { createVarBlocksNoEvents(this.workspace, ['id1']); - this.workspace.renameVariableById('id1', 'name2'); + const id1 = this.variableMap.getVariableById('id1'); + this.variableMap.renameVariable(id1, 'name2'); this.clock.runAll(); this.workspace.undo(); this.clock.runAll(); assertBlockVarModelName(this.workspace, 0, 'name1'); - assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); + assertVariableValues(this.variableMap, 'name1', 'type1', 'id1'); this.workspace.undo(true); this.clock.runAll(); assertBlockVarModelName(this.workspace, 0, 'name2'); - assertVariableValues(this.workspace, 'name2', 'type1', 'id1'); + assertVariableValues(this.variableMap, 'name2', 'type1', 'id1'); }); test('Reference exists different capitalization no usages rename to Name1', function () { - this.workspace.renameVariableById('id1', 'Name1'); + const id1 = this.variableMap.getVariableById('id1'); + this.variableMap.renameVariable(id1, 'Name1'); this.clock.runAll(); this.workspace.undo(); this.clock.runAll(); - assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); + assertVariableValues(this.variableMap, 'name1', 'type1', 'id1'); this.workspace.undo(true); this.clock.runAll(); - assertVariableValues(this.workspace, 'Name1', 'type1', 'id1'); + assertVariableValues(this.variableMap, 'Name1', 'type1', 'id1'); }); test('Reference exists different capitalization with usages rename to Name1', function () { createVarBlocksNoEvents(this.workspace, ['id1']); - this.workspace.renameVariableById('id1', 'Name1'); + const id1 = this.variableMap.getVariableById('id1'); + this.variableMap.renameVariable(id1, 'Name1'); this.clock.runAll(); this.workspace.undo(); this.clock.runAll(); assertBlockVarModelName(this.workspace, 0, 'name1'); - assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); + assertVariableValues(this.variableMap, 'name1', 'type1', 'id1'); this.workspace.undo(true); this.clock.runAll(); assertBlockVarModelName(this.workspace, 0, 'Name1'); - assertVariableValues(this.workspace, 'Name1', 'type1', 'id1'); + assertVariableValues(this.variableMap, 'Name1', 'type1', 'id1'); }); suite('Two variables rename overlap', function () { test('Same type no usages rename variable with id1 to name2', function () { - this.workspace.createVariable('name2', 'type1', 'id2'); - this.workspace.renameVariableById('id1', 'name2'); + this.variableMap.createVariable('name2', 'type1', 'id2'); + const id1 = this.variableMap.getVariableById('id1'); + this.variableMap.renameVariable(id1, 'name2'); this.clock.runAll(); this.workspace.undo(); this.clock.runAll(); - assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); - assertVariableValues(this.workspace, 'name2', 'type1', 'id2'); + assertVariableValues(this.variableMap, 'name1', 'type1', 'id1'); + assertVariableValues(this.variableMap, 'name2', 'type1', 'id2'); this.workspace.undo(true); this.clock.runAll(); - assertVariableValues(this.workspace, 'name2', 'type1', 'id2'); - assert.isNull(this.workspace.getVariableById('id1')); + assertVariableValues(this.variableMap, 'name2', 'type1', 'id2'); + assert.isNull(this.variableMap.getVariableById('id1')); }); test('Same type with usages rename variable with id1 to name2', function () { - this.workspace.createVariable('name2', 'type1', 'id2'); + const variable = this.variableMap.createVariable( + 'name2', + 'type1', + 'id2', + ); createVarBlocksNoEvents(this.workspace, ['id1', 'id2']); - this.workspace.renameVariableById('id1', 'name2'); + const id1 = this.variableMap.getVariableById('id1'); + this.variableMap.renameVariable(id1, 'name2'); this.clock.runAll(); this.workspace.undo(); this.clock.runAll(); assertBlockVarModelName(this.workspace, 0, 'name1'); assertBlockVarModelName(this.workspace, 1, 'name2'); - assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); - assertVariableValues(this.workspace, 'name2', 'type1', 'id2'); + assertVariableValues(this.variableMap, 'name1', 'type1', 'id1'); + assertVariableValues(this.variableMap, 'name2', 'type1', 'id2'); this.workspace.undo(true); this.clock.runAll(); - assertVariableValues(this.workspace, 'name2', 'type1', 'id2'); - assert.isNull(this.workspace.getVariableById('id1')); + assertVariableValues(this.variableMap, 'name2', 'type1', 'id2'); + assert.isNull(this.variableMap.getVariableById('id1')); }); test('Same type different capitalization no usages rename variable with id1 to Name2', function () { - this.workspace.createVariable('name2', 'type1', 'id2'); - this.workspace.renameVariableById('id1', 'Name2'); + this.variableMap.createVariable('name2', 'type1', 'id2'); + const id1 = this.variableMap.getVariableById('id1'); + this.variableMap.renameVariable(id1, 'Name2'); this.clock.runAll(); this.workspace.undo(); this.clock.runAll(); - assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); - assertVariableValues(this.workspace, 'name2', 'type1', 'id2'); + assertVariableValues(this.variableMap, 'name1', 'type1', 'id1'); + assertVariableValues(this.variableMap, 'name2', 'type1', 'id2'); this.workspace.undo(true); this.clock.runAll(); - assertVariableValues(this.workspace, 'Name2', 'type1', 'id2'); - assert.isNull(this.workspace.getVariable('name1')); + assertVariableValues(this.variableMap, 'Name2', 'type1', 'id2'); + assert.isNull(this.variableMap.getVariable('name1')); }); test('Same type different capitalization with usages rename variable with id1 to Name2', function () { - this.workspace.createVariable('name2', 'type1', 'id2'); + this.variableMap.createVariable('name2', 'type1', 'id2'); createVarBlocksNoEvents(this.workspace, ['id1', 'id2']); - this.workspace.renameVariableById('id1', 'Name2'); + const id1 = this.variableMap.getVariableById('id1'); + this.variableMap.renameVariable(id1, 'Name2'); this.clock.runAll(); this.workspace.undo(); this.clock.runAll(); assertBlockVarModelName(this.workspace, 0, 'name1'); assertBlockVarModelName(this.workspace, 1, 'name2'); - assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); - assertVariableValues(this.workspace, 'name2', 'type1', 'id2'); + assertVariableValues(this.variableMap, 'name1', 'type1', 'id1'); + assertVariableValues(this.variableMap, 'name2', 'type1', 'id2'); this.workspace.undo(true); this.clock.runAll(); - assertVariableValues(this.workspace, 'Name2', 'type1', 'id2'); - assert.isNull(this.workspace.getVariableById('id1')); + assertVariableValues(this.variableMap, 'Name2', 'type1', 'id2'); + assert.isNull(this.variableMap.getVariableById('id1')); assertBlockVarModelName(this.workspace, 0, 'Name2'); assertBlockVarModelName(this.workspace, 1, 'Name2'); }); test('Different type no usages rename variable with id1 to name2', function () { - this.workspace.createVariable('name2', 'type2', 'id2'); - this.workspace.renameVariableById('id1', 'name2'); + this.variableMap.createVariable('name2', 'type2', 'id2'); + const id1 = this.variableMap.getVariableById('id1'); + this.variableMap.renameVariable(id1, 'name2'); this.clock.runAll(); this.workspace.undo(); this.clock.runAll(); - assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); - assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); + assertVariableValues(this.variableMap, 'name1', 'type1', 'id1'); + assertVariableValues(this.variableMap, 'name2', 'type2', 'id2'); this.workspace.undo(true); this.clock.runAll(); - assertVariableValues(this.workspace, 'name2', 'type1', 'id1'); - assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); + assertVariableValues(this.variableMap, 'name2', 'type1', 'id1'); + assertVariableValues(this.variableMap, 'name2', 'type2', 'id2'); }); test('Different type with usages rename variable with id1 to name2', function () { - this.workspace.createVariable('name2', 'type2', 'id2'); + this.variableMap.createVariable('name2', 'type2', 'id2'); createVarBlocksNoEvents(this.workspace, ['id1', 'id2']); - this.workspace.renameVariableById('id1', 'name2'); + const id1 = this.variableMap.getVariableById('id1'); + this.variableMap.renameVariable(id1, 'name2'); this.clock.runAll(); this.workspace.undo(); this.clock.runAll(); - assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); - assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); + assertVariableValues(this.variableMap, 'name1', 'type1', 'id1'); + assertVariableValues(this.variableMap, 'name2', 'type2', 'id2'); assertBlockVarModelName(this.workspace, 0, 'name1'); assertBlockVarModelName(this.workspace, 1, 'name2'); this.workspace.undo(true); this.clock.runAll(); - assertVariableValues(this.workspace, 'name2', 'type1', 'id1'); - assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); + assertVariableValues(this.variableMap, 'name2', 'type1', 'id1'); + assertVariableValues(this.variableMap, 'name2', 'type2', 'id2'); assertBlockVarModelName(this.workspace, 0, 'name2'); assertBlockVarModelName(this.workspace, 1, 'name2'); }); test('Different type different capitalization no usages rename variable with id1 to Name2', function () { - this.workspace.createVariable('name2', 'type2', 'id2'); - this.workspace.renameVariableById('id1', 'Name2'); + this.variableMap.createVariable('name2', 'type2', 'id2'); + const id1 = this.variableMap.getVariableById('id1'); + this.variableMap.renameVariable(id1, 'Name2'); this.clock.runAll(); this.workspace.undo(); @@ -1660,27 +1627,28 @@ export function testAWorkspace() { this.workspace.undo(true); this.clock.runAll(); - assertVariableValues(this.workspace, 'Name2', 'type1', 'id1'); - assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); + assertVariableValues(this.variableMap, 'Name2', 'type1', 'id1'); + assertVariableValues(this.variableMap, 'name2', 'type2', 'id2'); }); test('Different type different capitalization with usages rename variable with id1 to Name2', function () { - this.workspace.createVariable('name2', 'type2', 'id2'); + this.variableMap.createVariable('name2', 'type2', 'id2'); createVarBlocksNoEvents(this.workspace, ['id1', 'id2']); - this.workspace.renameVariableById('id1', 'Name2'); + const id1 = this.variableMap.getVariableById('id1'); + this.variableMap.renameVariable(id1, 'Name2'); this.clock.runAll(); this.workspace.undo(); this.clock.runAll(); - assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); - assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); + assertVariableValues(this.variableMap, 'name1', 'type1', 'id1'); + assertVariableValues(this.variableMap, 'name2', 'type2', 'id2'); assertBlockVarModelName(this.workspace, 0, 'name1'); assertBlockVarModelName(this.workspace, 1, 'name2'); this.workspace.undo(true); this.clock.runAll(); - assertVariableValues(this.workspace, 'Name2', 'type1', 'id1'); - assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); + assertVariableValues(this.variableMap, 'Name2', 'type1', 'id1'); + assertVariableValues(this.variableMap, 'name2', 'type2', 'id2'); assertBlockVarModelName(this.workspace, 0, 'Name2'); assertBlockVarModelName(this.workspace, 1, 'name2'); }); diff --git a/tests/mocha/theme_test.js b/packages/blockly/tests/mocha/theme_test.js similarity index 99% rename from tests/mocha/theme_test.js rename to packages/blockly/tests/mocha/theme_test.js index 1f425dca6a3..f54641a348f 100644 --- a/tests/mocha/theme_test.js +++ b/packages/blockly/tests/mocha/theme_test.js @@ -5,7 +5,7 @@ */ import {EventType} from '../../build/src/core/events/type.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {assertEventFired} from './test_helpers/events.js'; import { sharedTestSetup, diff --git a/packages/blockly/tests/mocha/toast_test.js b/packages/blockly/tests/mocha/toast_test.js new file mode 100644 index 00000000000..afb7f7f6cb9 --- /dev/null +++ b/packages/blockly/tests/mocha/toast_test.js @@ -0,0 +1,129 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {assert} from '../../node_modules/chai/index.js'; +import { + sharedTestSetup, + sharedTestTeardown, +} from './test_helpers/setup_teardown.js'; + +suite('Toasts', function () { + setup(function () { + sharedTestSetup.call(this); + this.workspace = Blockly.inject('blocklyDiv', {}); + this.toastIsVisible = (message) => { + const toast = this.workspace + .getInjectionDiv() + .querySelector('.blocklyToast'); + return !!(toast && toast.textContent === message); + }; + }); + + teardown(function () { + sharedTestTeardown.call(this); + }); + + test('can be shown', function () { + const message = 'texas toast'; + Blockly.Toast.show(this.workspace, {message}); + assert.isTrue(this.toastIsVisible(message)); + }); + + test('can be shown only once per session', function () { + const options = { + message: 'texas toast', + id: 'test', + oncePerSession: true, + }; + Blockly.Toast.show(this.workspace, options); + assert.isTrue(this.toastIsVisible(options.message)); + Blockly.Toast.hide(this.workspace); + Blockly.Toast.show(this.workspace, options); + assert.isFalse(this.toastIsVisible(options.message)); + }); + + test('oncePerSession is ignored when false', function () { + const options = { + message: 'texas toast', + id: 'some id', + oncePerSession: true, + }; + Blockly.Toast.show(this.workspace, options); + assert.isTrue(this.toastIsVisible(options.message)); + Blockly.Toast.hide(this.workspace); + options.oncePerSession = false; + Blockly.Toast.show(this.workspace, options); + assert.isTrue(this.toastIsVisible(options.message)); + }); + + test('can be hidden', function () { + const message = 'texas toast'; + Blockly.Toast.show(this.workspace, {message}); + assert.isTrue(this.toastIsVisible(message)); + Blockly.Toast.hide(this.workspace); + assert.isFalse(this.toastIsVisible(message)); + }); + + test('can be hidden by ID', function () { + const message = 'texas toast'; + Blockly.Toast.show(this.workspace, {message, id: 'test'}); + assert.isTrue(this.toastIsVisible(message)); + Blockly.Toast.hide(this.workspace, 'test'); + assert.isFalse(this.toastIsVisible(message)); + }); + + test('hide does not hide toasts with different ID', function () { + const message = 'texas toast'; + Blockly.Toast.show(this.workspace, {message, id: 'test'}); + assert.isTrue(this.toastIsVisible(message)); + Blockly.Toast.hide(this.workspace, 'test2'); + assert.isTrue(this.toastIsVisible(message)); + }); + + test('are shown for the designated duration', function () { + const clock = sinon.useFakeTimers(); + + const message = 'texas toast'; + Blockly.Toast.show(this.workspace, {message, duration: 3}); + for (let i = 0; i < 3; i++) { + assert.isTrue(this.toastIsVisible(message)); + clock.tick(1000); + } + assert.isFalse(this.toastIsVisible(message)); + + clock.restore(); + }); + + test('default to polite assertiveness', function () { + const message = 'texas toast'; + Blockly.Toast.show(this.workspace, {message, id: 'test'}); + const toast = this.workspace + .getInjectionDiv() + .querySelector('.blocklyToast'); + + assert.equal( + toast.getAttribute('aria-live'), + Blockly.Toast.Assertiveness.POLITE, + ); + }); + + test('respects assertiveness option', function () { + const message = 'texas toast'; + Blockly.Toast.show(this.workspace, { + message, + id: 'test', + assertiveness: Blockly.Toast.Assertiveness.ASSERTIVE, + }); + const toast = this.workspace + .getInjectionDiv() + .querySelector('.blocklyToast'); + + assert.equal( + toast.getAttribute('aria-live'), + Blockly.Toast.Assertiveness.ASSERTIVE, + ); + }); +}); diff --git a/tests/mocha/toolbox_test.js b/packages/blockly/tests/mocha/toolbox_test.js similarity index 88% rename from tests/mocha/toolbox_test.js rename to packages/blockly/tests/mocha/toolbox_test.js index 3b69fac5dca..480fdfdc6fc 100644 --- a/tests/mocha/toolbox_test.js +++ b/packages/blockly/tests/mocha/toolbox_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {defineStackBlock} from './test_helpers/block_definitions.js'; import { sharedTestSetup, @@ -47,13 +47,14 @@ suite('Toolbox', function () { test('Init called -> HtmlDiv is inserted before parent node', function () { const toolboxDiv = Blockly.common.getMainWorkspace().getInjectionDiv() .childNodes[0]; - assert.equal(toolboxDiv.className, 'blocklyToolboxDiv'); + assert.equal(toolboxDiv.className, 'blocklyToolbox'); }); test('Init called -> Toolbox is subscribed to background and foreground colour', function () { const themeManager = this.toolbox.workspace_.getThemeManager(); const themeManagerSpy = sinon.spy(themeManager, 'subscribe'); const componentManager = this.toolbox.workspace_.getComponentManager(); sinon.stub(componentManager, 'addComponent'); + this.toolbox.dispose(); // Dispose of the old toolbox so that it can be reinited. this.toolbox.init(); sinon.assert.calledWith( themeManagerSpy, @@ -72,12 +73,14 @@ suite('Toolbox', function () { const renderSpy = sinon.spy(this.toolbox, 'render'); const componentManager = this.toolbox.workspace_.getComponentManager(); sinon.stub(componentManager, 'addComponent'); + this.toolbox.dispose(); // Dispose of the old toolbox so that it can be reinited. this.toolbox.init(); sinon.assert.calledOnce(renderSpy); }); test('Init called -> Flyout is initialized', function () { const componentManager = this.toolbox.workspace_.getComponentManager(); sinon.stub(componentManager, 'addComponent'); + this.toolbox.dispose(); // Dispose of the old toolbox so that it can be reinited. this.toolbox.init(); assert.isDefined(this.toolbox.getFlyout()); }); @@ -98,7 +101,7 @@ suite('Toolbox', function () { {'kind': 'category', 'contents': []}, ], }); - assert.lengthOf(this.toolbox.contents_, 2); + assert.equal(this.toolbox.contents.size, 2); sinon.assert.called(positionStub); }); // TODO: Uncomment once implemented. @@ -153,7 +156,7 @@ suite('Toolbox', function () { ], }; this.toolbox.render(jsonDef); - assert.lengthOf(this.toolbox.contents_, 1); + assert.equal(this.toolbox.contents.size, 1); }); test('multiple icon classes can be applied', function () { const jsonDef = { @@ -176,7 +179,58 @@ suite('Toolbox', function () { assert.doesNotThrow(() => { this.toolbox.render(jsonDef); }); - assert.lengthOf(this.toolbox.contents_, 1); + assert.equal(this.toolbox.contents.size, 1); + }); + }); + + suite('focus management', function () { + setup(function () { + this.toolbox = getInjectedToolbox(); + }); + teardown(function () { + this.toolbox.dispose(); + }); + + test('Losing focus hides autoclosing flyout', function () { + // Focus the toolbox and select a category to open the flyout. + const target = this.toolbox.HtmlDiv.querySelector( + '.blocklyToolboxCategory', + ); + Blockly.getFocusManager().focusNode(this.toolbox); + target.dispatchEvent( + new PointerEvent('pointerdown', { + target, + bubbles: true, + }), + ); + assert.isTrue(this.toolbox.getFlyout().isVisible()); + + // Focus the workspace to trigger the toolbox to close the flyout. + Blockly.getFocusManager().focusNode(this.toolbox.getWorkspace()); + assert.isFalse(this.toolbox.getFlyout().isVisible()); + }); + + test('Losing focus does not hide non-autoclosing flyout', function () { + // Make the toolbox's flyout non-autoclosing. + this.toolbox.getFlyout().setAutoClose(false); + + // Focus the toolbox and select a category to open the flyout. + const target = this.toolbox.HtmlDiv.querySelector( + '.blocklyToolboxCategory', + ); + Blockly.getFocusManager().focusNode(this.toolbox); + target.dispatchEvent( + new PointerEvent('pointerdown', { + target, + bubbles: true, + }), + ); + assert.isTrue(this.toolbox.getFlyout().isVisible()); + + // Focus the workspace; this should *not* trigger the toolbox to close the + // flyout, which should remain visible. + Blockly.getFocusManager().focusNode(this.toolbox.getWorkspace()); + assert.isTrue(this.toolbox.getFlyout().isVisible()); }); }); @@ -198,11 +252,13 @@ suite('Toolbox', function () { sinon.assert.calledOnce(hideChaffStub); }); test('Category clicked -> Should select category', function () { - const categoryXml = document.getElementsByClassName('blocklyTreeRow')[0]; + const categoryXml = document.getElementsByClassName( + 'blocklyToolboxCategory', + )[0]; const evt = { 'target': categoryXml, }; - const item = this.toolbox.contentMap_[categoryXml.getAttribute('id')]; + const item = this.toolbox.contents.get(categoryXml.getAttribute('id')); const setSelectedSpy = sinon.spy(this.toolbox, 'setSelectedItem'); const onClickSpy = sinon.spy(item, 'onClick'); this.toolbox.onClick_(evt); @@ -354,14 +410,16 @@ suite('Toolbox', function () { assert.isFalse(handled); }); test('Next item is selectable -> Should select next item', function () { - const item = this.toolbox.contents_[0]; + const items = [...this.toolbox.contents.values()]; + const item = items[0]; this.toolbox.selectedItem_ = item; const handled = this.toolbox.selectNext(); assert.isTrue(handled); - assert.equal(this.toolbox.selectedItem_, this.toolbox.contents_[1]); + assert.equal(this.toolbox.selectedItem_, items[1]); }); test('Selected item is last item -> Should not handle event', function () { - const item = this.toolbox.contents_[this.toolbox.contents_.length - 1]; + const items = [...this.toolbox.contents.values()]; + const item = items.at(-1); this.toolbox.selectedItem_ = item; const handled = this.toolbox.selectNext(); assert.isFalse(handled); @@ -385,15 +443,16 @@ suite('Toolbox', function () { assert.isFalse(handled); }); test('Selected item is first item -> Should not handle event', function () { - const item = this.toolbox.contents_[0]; + const item = [...this.toolbox.contents.values()][0]; this.toolbox.selectedItem_ = item; const handled = this.toolbox.selectPrevious(); assert.isFalse(handled); assert.equal(this.toolbox.selectedItem_, item); }); test('Previous item is selectable -> Should select previous item', function () { - const item = this.toolbox.contents_[1]; - const prevItem = this.toolbox.contents_[0]; + const items = [...this.toolbox.contents.values()]; + const item = items[1]; + const prevItem = items[0]; this.toolbox.selectedItem_ = item; const handled = this.toolbox.selectPrevious(); assert.isTrue(handled); @@ -402,9 +461,10 @@ suite('Toolbox', function () { test('Previous item is collapsed -> Should skip over children of the previous item', function () { const childItem = getChildItem(this.toolbox); const parentItem = childItem.getParent(); - const parentIdx = this.toolbox.contents_.indexOf(parentItem); + const items = [...this.toolbox.contents.values()]; + const parentIdx = items.indexOf(parentItem); // Gets the item after the parent. - const item = this.toolbox.contents_[parentIdx + 1]; + const item = items[parentIdx + 1]; this.toolbox.selectedItem_ = item; const handled = this.toolbox.selectPrevious(); assert.isTrue(handled); @@ -726,9 +786,10 @@ suite('Toolbox', function () { }); test('Child categories visible if all ancestors expanded', function () { this.toolbox.render(getDeeplyNestedJSON()); - const outerCategory = this.toolbox.contents_[0]; - const middleCategory = this.toolbox.contents_[1]; - const innerCategory = this.toolbox.contents_[2]; + const items = [...this.toolbox.contents.values()]; + const outerCategory = items[0]; + const middleCategory = items[1]; + const innerCategory = items[2]; outerCategory.toggleExpanded(); middleCategory.toggleExpanded(); @@ -741,8 +802,9 @@ suite('Toolbox', function () { }); test('Child categories not visible if any ancestor not expanded', function () { this.toolbox.render(getDeeplyNestedJSON()); - const middleCategory = this.toolbox.contents_[1]; - const innerCategory = this.toolbox.contents_[2]; + const items = [...this.toolbox.contents.values()]; + const middleCategory = items[1]; + const innerCategory = items[2]; // Don't expand the outermost category // Even though the direct parent of inner is expanded, it shouldn't be visible diff --git a/tests/mocha/tooltip_test.js b/packages/blockly/tests/mocha/tooltip_test.js similarity index 99% rename from tests/mocha/tooltip_test.js rename to packages/blockly/tests/mocha/tooltip_test.js index 1edc8ad6e25..0695b9ebe03 100644 --- a/tests/mocha/tooltip_test.js +++ b/packages/blockly/tests/mocha/tooltip_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/touch_test.js b/packages/blockly/tests/mocha/touch_test.js similarity index 98% rename from tests/mocha/touch_test.js rename to packages/blockly/tests/mocha/touch_test.js index 775665643b7..30a9fe72724 100644 --- a/tests/mocha/touch_test.js +++ b/packages/blockly/tests/mocha/touch_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, diff --git a/tests/mocha/trashcan_test.js b/packages/blockly/tests/mocha/trashcan_test.js similarity index 99% rename from tests/mocha/trashcan_test.js rename to packages/blockly/tests/mocha/trashcan_test.js index 5486326f1e0..d96e00f3a21 100644 --- a/tests/mocha/trashcan_test.js +++ b/packages/blockly/tests/mocha/trashcan_test.js @@ -6,7 +6,7 @@ import {EventType} from '../../build/src/core/events/type.js'; import * as eventUtils from '../../build/src/core/events/utils.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { defineBasicBlockWithField, defineMutatorBlocks, diff --git a/tests/mocha/utils_test.js b/packages/blockly/tests/mocha/utils_test.js similarity index 96% rename from tests/mocha/utils_test.js rename to packages/blockly/tests/mocha/utils_test.js index 97984fdbae4..accf164b79b 100644 --- a/tests/mocha/utils_test.js +++ b/packages/blockly/tests/mocha/utils_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, @@ -533,4 +533,25 @@ suite('Utils', function () { assert.equal(Blockly.utils.math.toDegrees(5 * quarter), 360 + 90, '450'); }); }); + + suite('deepMerge', function () { + test('Merges two objects', function () { + const target = {a: 1, b: '2', shared: 'this should be overwritten'}; + const source = {c: {deeplyNested: true}, shared: 'I overwrote it'}; + + const expected = {...target, ...source}; + const actual = Blockly.utils.object.deepMerge(target, source); + + assert.deepEqual(expected, actual); + }); + test('Merges objects with arrays', function () { + const target = {a: 1}; + const source = {b: ['orange', 'lime']}; + + const expected = {...target, ...source}; + const actual = Blockly.utils.object.deepMerge(target, source); + + assert.deepEqual(expected, actual); + }); + }); }); diff --git a/tests/mocha/variable_map_test.js b/packages/blockly/tests/mocha/variable_map_test.js similarity index 79% rename from tests/mocha/variable_map_test.js rename to packages/blockly/tests/mocha/variable_map_test.js index 51f710c9921..505a221bf80 100644 --- a/tests/mocha/variable_map_test.js +++ b/packages/blockly/tests/mocha/variable_map_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { assertEventFired, assertEventNotFired, @@ -21,7 +21,7 @@ suite('Variable Map', function () { setup(function () { sharedTestSetup.call(this); this.workspace = new Blockly.Workspace(); - this.variableMap = new Blockly.VariableMap(this.workspace); + this.variableMap = this.workspace.getVariableMap(); }); teardown(function () { @@ -39,17 +39,17 @@ suite('Variable Map', function () { this.variableMap.createVariable('name1', 'type1', 'id1'); // Assert there is only one variable in the this.variableMap. - let keys = Array.from(this.variableMap.variableMap.keys()); + let keys = this.variableMap.getTypes(); assert.equal(keys.length, 1); - let varMapLength = this.variableMap.variableMap.get(keys[0]).length; + let varMapLength = this.variableMap.getVariablesOfType(keys[0]).length; assert.equal(varMapLength, 1); this.variableMap.createVariable('name1', 'type1'); assertVariableValues(this.variableMap, 'name1', 'type1', 'id1'); // Check that the size of the variableMap did not change. - keys = Array.from(this.variableMap.variableMap.keys()); + keys = this.variableMap.getTypes(); assert.equal(keys.length, 1); - varMapLength = this.variableMap.variableMap.get(keys[0]).length; + varMapLength = this.variableMap.getVariablesOfType(keys[0]).length; assert.equal(varMapLength, 1); }); @@ -59,16 +59,16 @@ suite('Variable Map', function () { this.variableMap.createVariable('name1', 'type1', 'id1'); // Assert there is only one variable in the this.variableMap. - let keys = Array.from(this.variableMap.variableMap.keys()); + let keys = this.variableMap.getTypes(); assert.equal(keys.length, 1); - const varMapLength = this.variableMap.variableMap.get(keys[0]).length; + const varMapLength = this.variableMap.getVariablesOfType(keys[0]).length; assert.equal(varMapLength, 1); this.variableMap.createVariable('name1', 'type2', 'id2'); assertVariableValues(this.variableMap, 'name1', 'type1', 'id1'); assertVariableValues(this.variableMap, 'name1', 'type2', 'id2'); // Check that the size of the variableMap did change. - keys = Array.from(this.variableMap.variableMap.keys()); + keys = this.variableMap.getTypes(); assert.equal(keys.length, 2); }); @@ -187,24 +187,6 @@ suite('Variable Map', function () { }); }); - suite('getVariableTypes', function () { - test('Trivial', function () { - this.variableMap.createVariable('name1', 'type1', 'id1'); - this.variableMap.createVariable('name2', 'type1', 'id2'); - this.variableMap.createVariable('name3', 'type2', 'id3'); - this.variableMap.createVariable('name4', 'type3', 'id4'); - const resultArray = this.variableMap.getVariableTypes(); - // The empty string is always an option. - assert.deepEqual(resultArray, ['type1', 'type2', 'type3', '']); - }); - - test('None', function () { - // The empty string is always an option. - const resultArray = this.variableMap.getVariableTypes(); - assert.deepEqual(resultArray, ['']); - }); - }); - suite('getVariablesOfType', function () { test('Trivial', function () { const var1 = this.variableMap.createVariable('name1', 'type1', 'id1'); @@ -246,6 +228,72 @@ suite('Variable Map', function () { }); }); + suite( + 'Using changeVariableType to change the type of a variable', + function () { + test('updates it to a new non-empty value', function () { + const variable = this.variableMap.createVariable( + 'name1', + 'type1', + 'id1', + ); + this.variableMap.changeVariableType(variable, 'type2'); + const oldTypeVariables = this.variableMap.getVariablesOfType('type1'); + const newTypeVariables = this.variableMap.getVariablesOfType('type2'); + assert.deepEqual(oldTypeVariables, []); + assert.deepEqual(newTypeVariables, [variable]); + assert.equal(variable.getType(), 'type2'); + }); + + test('updates it to a new empty value', function () { + const variable = this.variableMap.createVariable( + 'name1', + 'type1', + 'id1', + ); + this.variableMap.changeVariableType(variable, ''); + const oldTypeVariables = this.variableMap.getVariablesOfType('type1'); + const newTypeVariables = this.variableMap.getVariablesOfType(''); + assert.deepEqual(oldTypeVariables, []); + assert.deepEqual(newTypeVariables, [variable]); + assert.equal(variable.getType(), ''); + }); + + test('removes the type from the map when the last instance is changed', function () { + const var1 = this.variableMap.createVariable('name1', 'type1'); + const var2 = this.variableMap.createVariable('name2', 'type2'); + this.variableMap.changeVariableType(var1, 'type2'); + assert.deepEqual(this.variableMap.getTypes(), ['type2']); + }); + }, + ); + + suite('addVariable', function () { + test('normally', function () { + const variable = new Blockly.VariableModel(this.workspace, 'foo', 'int'); + assert.isNull(this.variableMap.getVariableById(variable.getId())); + this.variableMap.addVariable(variable); + assert.equal( + this.variableMap.getVariableById(variable.getId()), + variable, + ); + }); + }); + + suite('getTypes', function () { + test('when map is empty', function () { + const types = this.variableMap.getTypes(); + assert.deepEqual(types, []); + }); + + test('with various types', function () { + this.variableMap.createVariable('name1', 'type1', 'id1'); + this.variableMap.createVariable('name2', '', 'id2'); + const types = this.variableMap.getTypes(); + assert.deepEqual(types, ['type1', '']); + }); + }); + suite('getAllVariables', function () { test('Trivial', function () { const var1 = this.variableMap.createVariable('name1', 'type1', 'id1'); @@ -340,35 +388,6 @@ suite('Variable Map', function () { ); }); }); - - suite('deleting by ID', function () { - test('delete events are fired when a variable is deleted', function () { - this.variableMap.createVariable('test name', 'test type', 'test id'); - this.variableMap.deleteVariableById('test id'); - - assertEventFired( - this.eventSpy, - Blockly.Events.VarDelete, - { - varType: 'test type', - varName: 'test name', - varId: 'test id', - }, - this.workspace.id, - ); - }); - - test('delete events are not fired when a variable does not exist', function () { - this.variableMap.deleteVariableById('test id'); - - assertEventNotFired( - this.eventSpy, - Blockly.Events.VarDelete, - {}, - this.workspace.id, - ); - }); - }); }); suite('variable rename events', function () { @@ -425,43 +444,26 @@ suite('Variable Map', function () { ); }); }); + }); - suite('renaming by ID', function () { - test('rename events are fired when a variable is renamed', function () { - this.variableMap.createVariable('test name', 'test type', 'test id'); - this.variableMap.renameVariableById('test id', 'new test name'); - - assertEventFired( - this.eventSpy, - Blockly.Events.VarRename, - { - oldName: 'test name', - newName: 'new test name', - varId: 'test id', - }, - this.workspace.id, - ); - }); - - test('rename events are not fired if the variable name already matches', function () { - this.variableMap.createVariable('test name', 'test type', 'test id'); - this.variableMap.renameVariableById('test id', 'test name'); - - assertEventNotFired( - this.eventSpy, - Blockly.Events.VarRename, - {}, - this.workspace.id, - ); - }); - - test('renaming throws if the variable does not exist', function () { - // Not sure why this throws when the other one doesn't but might - // as well test it. - assert.throws(() => { - this.variableMap.renameVariableById('test id', 'test name'); - }, `Tried to rename a variable that didn't exist`); - }); + suite('variable type change events', function () { + test('are fired when a variable has its type changed', function () { + const variable = this.variableMap.createVariable( + 'name1', + 'type1', + 'id1', + ); + this.variableMap.changeVariableType(variable, 'type2'); + assertEventFired( + this.eventSpy, + Blockly.Events.VarTypeChange, + { + oldType: 'type1', + newType: 'type2', + varId: 'id1', + }, + this.workspace.id, + ); }); }); }); diff --git a/tests/mocha/variable_model_test.js b/packages/blockly/tests/mocha/variable_model_test.js similarity index 74% rename from tests/mocha/variable_model_test.js rename to packages/blockly/tests/mocha/variable_model_test.js index 4ac533b65a9..eee7ea9bf41 100644 --- a/tests/mocha/variable_model_test.js +++ b/packages/blockly/tests/mocha/variable_model_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, @@ -27,8 +27,8 @@ suite('Variable Model', function () { 'test_type', 'test_id', ); - assert.equal(variable.name, 'test'); - assert.equal(variable.type, 'test_type'); + assert.equal(variable.getName(), 'test'); + assert.equal(variable.getType(), 'test_type'); assert.equal(variable.getId(), 'test_id'); }); @@ -39,7 +39,7 @@ suite('Variable Model', function () { null, 'test_id', ); - assert.equal(variable.type, ''); + assert.equal(variable.getType(), ''); }); test('Undefined type', function () { @@ -49,7 +49,7 @@ suite('Variable Model', function () { undefined, 'test_id', ); - assert.equal(variable.type, ''); + assert.equal(variable.getType(), ''); }); test('Null id', function () { @@ -59,8 +59,8 @@ suite('Variable Model', function () { 'test_type', null, ); - assert.equal(variable.name, 'test'); - assert.equal(variable.type, 'test_type'); + assert.equal(variable.getName(), 'test'); + assert.equal(variable.getType(), 'test_type'); assert.exists(variable.getId()); }); @@ -71,15 +71,15 @@ suite('Variable Model', function () { 'test_type', undefined, ); - assert.equal(variable.name, 'test'); - assert.equal(variable.type, 'test_type'); + assert.equal(variable.getName(), 'test'); + assert.equal(variable.getType(), 'test_type'); assert.exists(variable.getId()); }); test('Only name provided', function () { const variable = new Blockly.VariableModel(this.workspace, 'test'); - assert.equal(variable.name, 'test'); - assert.equal(variable.type, ''); + assert.equal(variable.getName(), 'test'); + assert.equal(variable.getType(), ''); assert.exists(variable.getId()); }); }); diff --git a/tests/mocha/webdriver.js b/packages/blockly/tests/mocha/webdriver.js similarity index 78% rename from tests/mocha/webdriver.js rename to packages/blockly/tests/mocha/webdriver.js index 207917c5e6b..06e7a3e6585 100644 --- a/tests/mocha/webdriver.js +++ b/packages/blockly/tests/mocha/webdriver.js @@ -15,9 +15,12 @@ const {posixPath} = require('../../scripts/helpers'); * Runs the Mocha tests in this directory in Chrome. It uses webdriverio to * launch Chrome and load index.html. Outputs a summary of the test results * to the console. + * + * @param {boolean} exitOnCompletetion True if the browser should automatically + * quit after tests have finished running. * @return {number} 0 on success, 1 on failure. */ -async function runMochaTestsInBrowser() { +async function runMochaTestsInBrowser(exitOnCompletion = true) { const options = { capabilities: { browserName: 'chrome', @@ -45,6 +48,17 @@ async function runMochaTestsInBrowser() { console.log('Loading URL: ' + url); await browser.url(url); + // Toggle the devtools setting to emulate focus, so that the window will + // always act as if it has focus regardless of the state of the window manager + // or operating system. This improves the reliability of FocusManager-related + // tests. + const puppeteer = await browser.getPuppeteer(); + await browser.call(async () => { + const page = (await puppeteer.pages())[0]; + const session = await page.createCDPSession(); + await session.send('Emulation.setFocusEmulationEnabled', { enabled: true }); + }); + await browser.waitUntil(async() => { const elem = await browser.$('#failureCount'); const text = await elem.getAttribute('tests_failed'); @@ -74,7 +88,7 @@ async function runMochaTestsInBrowser() { if (parseInt(numOfFailure) !== 0) { return 1; } - await browser.deleteSession(); + if (exitOnCompletion) await browser.deleteSession(); return 0; } diff --git a/tests/mocha/widget_div_test.js b/packages/blockly/tests/mocha/widget_div_test.js similarity index 55% rename from tests/mocha/widget_div_test.js rename to packages/blockly/tests/mocha/widget_div_test.js index 94fb855392d..4ad31f96c72 100644 --- a/tests/mocha/widget_div_test.js +++ b/packages/blockly/tests/mocha/widget_div_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { sharedTestSetup, sharedTestTeardown, @@ -13,9 +13,26 @@ import { suite('WidgetDiv', function () { setup(function () { sharedTestSetup.call(this); + this.workspace = Blockly.inject('blocklyDiv'); + this.setUpBlockWithField = function () { + const blockJson = { + 'type': 'text', + 'id': 'block_id', + 'x': 10, + 'y': 20, + 'fields': { + 'TEXT': '', + }, + }; + Blockly.serialization.blocks.append(blockJson, this.workspace); + return this.workspace.getBlockById('block_id'); + }; + // The workspace needs to be visible for focus-specific tests. + document.getElementById('blocklyDiv').style.visibility = 'visible'; }); teardown(function () { sharedTestTeardown.call(this); + document.getElementById('blocklyDiv').style.visibility = 'hidden'; }); suite('positionWithAnchor', function () { @@ -269,4 +286,142 @@ suite('WidgetDiv', function () { }); }); }); + + suite('Keyboard Shortcuts', function () { + test('Escape dismisses WidgetDiv', function () { + let hidden = false; + Blockly.WidgetDiv.show( + this, + false, + () => { + hidden = true; + }, + this.workspace, + false, + ); + assert.isFalse(hidden); + Blockly.WidgetDiv.getDiv().dispatchEvent( + new KeyboardEvent('keydown', { + key: 'Escape', + keyCode: 27, // example values. + }), + ); + assert.isTrue(hidden); + }); + }); + + suite('show()', function () { + test('shows nowhere', function () { + const block = this.setUpBlockWithField(); + const field = Array.from(block.getFields())[0]; + + Blockly.WidgetDiv.show(field, false, () => {}); + + // By default the div will not have a position. + const widgetDivElem = document.querySelector('.blocklyWidgetDiv'); + assert.strictEqual(widgetDivElem.style.display, 'block'); + assert.strictEqual(widgetDivElem.style.left, ''); + assert.strictEqual(widgetDivElem.style.top, ''); + }); + + test('with hide callback does not call callback', function () { + const block = this.setUpBlockWithField(); + const field = Array.from(block.getFields())[0]; + const onHideCallback = sinon.stub(); + + Blockly.WidgetDiv.show(field, false, () => {}); + + // Simply showing the div should never call the hide callback. + assert.strictEqual(onHideCallback.callCount, 0); + }); + + test('without managed ephemeral focus does not change focused node', function () { + const block = this.setUpBlockWithField(); + const field = Array.from(block.getFields())[0]; + Blockly.getFocusManager().focusNode(block); + + Blockly.WidgetDiv.show(field, false, () => {}, null, false); + + // Since managing ephemeral focus is disabled the current focused node shouldn't be changed. + const blockFocusableElem = block.getFocusableElement(); + assert.strictEqual(Blockly.getFocusManager().getFocusedNode(), block); + assert.strictEqual(document.activeElement, blockFocusableElem); + }); + + test('with managed ephemeral focus focuses widget div', function () { + const block = this.setUpBlockWithField(); + const field = Array.from(block.getFields())[0]; + Blockly.getFocusManager().focusNode(block); + + Blockly.WidgetDiv.show(field, false, () => {}, null, true); + + // Managing ephemeral focus won't change getFocusedNode() but will change the actual element + // with DOM focus. + const widgetDivElem = document.querySelector('.blocklyWidgetDiv'); + assert.strictEqual(Blockly.getFocusManager().getFocusedNode(), block); + assert.strictEqual(document.activeElement, widgetDivElem); + }); + }); + + suite('hide()', function () { + test('initially keeps display empty', function () { + Blockly.WidgetDiv.hide(); + + // The display property starts as empty and stays that way until an owner is attached. + const widgetDivElem = document.querySelector('.blocklyWidgetDiv'); + assert.strictEqual(widgetDivElem.style.display, ''); + }); + + test('for showing div hides div', function () { + const block = this.setUpBlockWithField(); + const field = Array.from(block.getFields())[0]; + Blockly.WidgetDiv.show(field, false, () => {}); + + Blockly.WidgetDiv.hide(); + + // Technically this will trigger a CSS animation, but the property is still set to 0. + const widgetDivElem = document.querySelector('.blocklyWidgetDiv'); + assert.strictEqual(widgetDivElem.style.display, 'none'); + }); + + test('for showing div and hide callback calls callback', function () { + const block = this.setUpBlockWithField(); + const field = Array.from(block.getFields())[0]; + const onHideCallback = sinon.stub(); + Blockly.WidgetDiv.show(field, false, onHideCallback); + + Blockly.WidgetDiv.hide(); + + // Hiding the div should trigger the hide callback. + assert.strictEqual(onHideCallback.callCount, 1); + }); + + test('for showing div without ephemeral focus does not change focus', function () { + const block = this.setUpBlockWithField(); + const field = Array.from(block.getFields())[0]; + Blockly.getFocusManager().focusNode(block); + Blockly.WidgetDiv.show(field, false, () => {}, null, false); + + Blockly.WidgetDiv.hide(); + + // Hiding the div shouldn't change what would have already been focused. + const blockFocusableElem = block.getFocusableElement(); + assert.strictEqual(Blockly.getFocusManager().getFocusedNode(), block); + assert.strictEqual(document.activeElement, blockFocusableElem); + }); + + test('for showing div with ephemeral focus restores DOM focus', function () { + const block = this.setUpBlockWithField(); + const field = Array.from(block.getFields())[0]; + Blockly.getFocusManager().focusNode(block); + Blockly.WidgetDiv.show(field, false, () => {}, null, true); + + Blockly.WidgetDiv.hide(); + + // Hiding the div should restore focus back to the block. + const blockFocusableElem = block.getFocusableElement(); + assert.strictEqual(Blockly.getFocusManager().getFocusedNode(), block); + assert.strictEqual(document.activeElement, blockFocusableElem); + }); + }); }); diff --git a/tests/mocha/workspace_comment_test.js b/packages/blockly/tests/mocha/workspace_comment_test.js similarity index 82% rename from tests/mocha/workspace_comment_test.js rename to packages/blockly/tests/mocha/workspace_comment_test.js index 6e3fa9607a0..fd4c94f6238 100644 --- a/tests/mocha/workspace_comment_test.js +++ b/packages/blockly/tests/mocha/workspace_comment_test.js @@ -4,6 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import {assert} from '../../node_modules/chai/index.js'; import { assertEventFired, createChangeListenerSpy, @@ -168,4 +169,29 @@ suite('Workspace comment', function () { ); }); }); + + suite('Focus', function () { + test('moves to the workspace when deleted', function () { + const comment = new Blockly.comments.RenderedWorkspaceComment( + this.workspace, + ); + Blockly.getFocusManager().focusNode(comment); + assert.equal(Blockly.getFocusManager().getFocusedNode(), comment); + comment.view.getCommentBarButtons()[1].performAction(); + assert.equal(Blockly.getFocusManager().getFocusedNode(), this.workspace); + }); + + test('does not change the layer', function () { + const comment = new Blockly.comments.RenderedWorkspaceComment( + this.workspace, + ); + + this.workspace.getLayerManager()?.moveToDragLayer(comment); + Blockly.getFocusManager().focusNode(comment); + assert.equal( + comment.getSvgRoot().parentElement, + this.workspace.getLayerManager()?.getDragLayer(), + ); + }); + }); }); diff --git a/tests/mocha/workspace_svg_test.js b/packages/blockly/tests/mocha/workspace_svg_test.js similarity index 98% rename from tests/mocha/workspace_svg_test.js rename to packages/blockly/tests/mocha/workspace_svg_test.js index 207cad45dc7..b40a46941bd 100644 --- a/tests/mocha/workspace_svg_test.js +++ b/packages/blockly/tests/mocha/workspace_svg_test.js @@ -5,7 +5,7 @@ */ import {EventType} from '../../build/src/core/events/type.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {defineStackBlock} from './test_helpers/block_definitions.js'; import { assertEventFired, @@ -16,6 +16,7 @@ import { sharedTestSetup, sharedTestTeardown, } from './test_helpers/setup_teardown.js'; +import {dispatchPointerEvent} from './test_helpers/user_input.js'; import {testAWorkspace} from './test_helpers/workspace.js'; suite('WorkspaceSvg', function () { @@ -114,6 +115,17 @@ suite('WorkspaceSvg', function () { assert.equal(true, shadowBlock.isDeadOrDying()); }); + test('getGesture returns null when no gesture is in progress', function () { + const gesture = this.workspace.getGesture(); + assert.isNull(gesture); + }); + + test('getGesture returns the current gesture when one is in progress', function () { + dispatchPointerEvent(this.workspace.getSvgGroup(), 'pointerdown'); + const gesture = this.workspace.getGesture(); + assert.isNotNull(gesture); + }); + suite('updateToolbox', function () { test('Passes in null when toolbox exists', function () { assert.throws( diff --git a/tests/mocha/workspace_test.js b/packages/blockly/tests/mocha/workspace_test.js similarity index 100% rename from tests/mocha/workspace_test.js rename to packages/blockly/tests/mocha/workspace_test.js diff --git a/tests/mocha/xml_test.js b/packages/blockly/tests/mocha/xml_test.js similarity index 93% rename from tests/mocha/xml_test.js rename to packages/blockly/tests/mocha/xml_test.js index c3ca2d4162e..94219f9a067 100644 --- a/tests/mocha/xml_test.js +++ b/packages/blockly/tests/mocha/xml_test.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import { addBlockTypeToCleanup, createGenUidStubWithReturns, @@ -119,6 +119,7 @@ suite('XML', function () { suite('blockToDom', function () { setup(function () { this.workspace = new Blockly.Workspace(); + this.variableMap = this.workspace.getVariableMap(); }); teardown(function () { workspaceTeardown.call(this, this.workspace); @@ -296,7 +297,7 @@ suite('XML', function () { ]); }); test('Variable Trivial', function () { - this.workspace.createVariable('name1', '', 'id1'); + this.variableMap.createVariable('name1', '', 'id1'); const block = new Blockly.Block( this.workspace, 'field_variable_test_block', @@ -306,7 +307,7 @@ suite('XML', function () { assertVariableDomField(resultFieldDom, 'VAR', null, 'id1', 'name1'); }); test('Variable Typed', function () { - this.workspace.createVariable('name1', 'string', 'id1'); + this.variableMap.createVariable('name1', 'string', 'id1'); const block = new Blockly.Block( this.workspace, 'field_variable_test_block', @@ -323,7 +324,7 @@ suite('XML', function () { }); test('Variable Default Case', function () { createGenUidStubWithReturns('1'); - this.workspace.createVariable('name1'); + this.variableMap.createVariable('name1'); Blockly.Events.disable(); const block = new Blockly.Block( @@ -439,15 +440,16 @@ suite('XML', function () { ], }, ]); + this.variableMap = this.workspace.getVariableMap(); }); teardown(function () { workspaceTeardown.call(this, this.workspace); }); test('One Variable', function () { createGenUidStubWithReturns('1'); - this.workspace.createVariable('name1'); + this.variableMap.createVariable('name1'); const resultDom = Blockly.Xml.variablesToDom( - this.workspace.getAllVariables(), + this.workspace.getVariableMap().getAllVariables(), ); assert.equal(resultDom.children.length, 1); const resultVariableDom = resultDom.children[0]; @@ -456,8 +458,8 @@ suite('XML', function () { assert.equal(resultVariableDom.getAttribute('id'), '1'); }); test('Two Variable one block', function () { - this.workspace.createVariable('name1', '', 'id1'); - this.workspace.createVariable('name2', 'type2', 'id2'); + this.variableMap.createVariable('name1', '', 'id1'); + this.variableMap.createVariable('name2', 'type2', 'id2'); // If events are enabled during block construction, it will create a // default variable. Blockly.Events.disable(); @@ -469,7 +471,7 @@ suite('XML', function () { Blockly.Events.enable(); const resultDom = Blockly.Xml.variablesToDom( - this.workspace.getAllVariables(), + this.workspace.getVariableMap().getAllVariables(), ); assert.equal(resultDom.children.length, 2); assertVariableDom(resultDom.children[0], null, 'id1', 'name1'); @@ -477,7 +479,7 @@ suite('XML', function () { }); test('No variables', function () { const resultDom = Blockly.Xml.variablesToDom( - this.workspace.getAllVariables(), + this.workspace.getVariableMap().getAllVariables(), ); assert.equal(resultDom.children.length, 0); }); @@ -531,28 +533,6 @@ suite('XML', function () { teardown(function () { workspaceTeardown.call(this, this.workspace); }); - suite('Dynamic Category Blocks', function () { - test('Untyped Variables', function () { - this.workspace.createVariable('name1', '', 'id1'); - const blocksArray = Blockly.Variables.flyoutCategoryBlocks( - this.workspace, - ); - for (let i = 0, xml; (xml = blocksArray[i]); i++) { - Blockly.Xml.domToBlock(xml, this.workspace); - } - }); - test('Typed Variables', function () { - this.workspace.createVariable('name1', 'String', 'id1'); - this.workspace.createVariable('name2', 'Number', 'id2'); - this.workspace.createVariable('name3', 'Colour', 'id3'); - const blocksArray = Blockly.VariablesDynamic.flyoutCategoryBlocks( - this.workspace, - ); - for (let i = 0, xml; (xml = blocksArray[i]); i++) { - Blockly.Xml.domToBlock(xml, this.workspace); - } - }); - }); suite('Comments', function () { suite('Headless', function () { test('Text', function () { @@ -910,36 +890,4 @@ suite('XML', function () { }); }); }); - suite('generateVariableFieldDom', function () { - test('Case Sensitive', function () { - const varId = 'testId'; - const type = 'testType'; - const name = 'testName'; - - const mockVariableModel = { - type: type, - name: name, - getId: function () { - return varId; - }, - }; - - const generatedXml = Blockly.Xml.domToText( - Blockly.Variables.generateVariableFieldDom(mockVariableModel), - ); - const expectedXml = - '' + - name + - ''; - assert.equal(generatedXml, expectedXml); - }); - }); }); diff --git a/tests/mocha/zoom_controls_test.js b/packages/blockly/tests/mocha/zoom_controls_test.js similarity index 97% rename from tests/mocha/zoom_controls_test.js rename to packages/blockly/tests/mocha/zoom_controls_test.js index dedc36b75b4..d9bb0f91e9b 100644 --- a/tests/mocha/zoom_controls_test.js +++ b/packages/blockly/tests/mocha/zoom_controls_test.js @@ -5,7 +5,7 @@ */ import {EventType} from '../../build/src/core/events/type.js'; -import {assert} from '../../node_modules/chai/chai.js'; +import {assert} from '../../node_modules/chai/index.js'; import {assertEventFired, assertEventNotFired} from './test_helpers/events.js'; import { sharedTestSetup, diff --git a/tests/multi_playground.html b/packages/blockly/tests/multi_playground.html similarity index 100% rename from tests/multi_playground.html rename to packages/blockly/tests/multi_playground.html diff --git a/tests/node/.mocharc.js b/packages/blockly/tests/node/.mocharc.js similarity index 100% rename from tests/node/.mocharc.js rename to packages/blockly/tests/node/.mocharc.js diff --git a/tests/node/node_modules/blockly-test b/packages/blockly/tests/node/node_modules/blockly-test similarity index 100% rename from tests/node/node_modules/blockly-test rename to packages/blockly/tests/node/node_modules/blockly-test diff --git a/packages/blockly/tests/node/run_node_test.mjs b/packages/blockly/tests/node/run_node_test.mjs new file mode 100644 index 00000000000..c6d21506987 --- /dev/null +++ b/packages/blockly/tests/node/run_node_test.mjs @@ -0,0 +1,191 @@ +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** @fileoverview Mocha tests that test Blockly in Node. */ + +console.log(process.cwd()); + +// N.B. the file ./node_modules/blockly-test should be a symlink to +// RELEASE_DIR (i.e. dist/) so that require will load the packaged +// version of blockly as if it were an external dependency. +// +// Moreover, (as with the typescript tests) this link has to be +// called something other than "blockly", because the node module +// resolution will favour loading the nearest enclosing package +// of the same name, which means that require('blockly') will load +// based on the exports section of the package.json in the repository +// root, but this fails because (at the time of writing) those paths +// are relative to RELEASE_DIR (dist/, into which package.json is +// copied when packaged), resulting in require() looking for the +// compressed bundles in the wrong place. + +import * as Blockly from 'blockly-test'; +import {javascriptGenerator} from 'blockly-test/javascript'; +import {assert} from 'chai'; + +const xmlText = + '\n' + + ' \n' + + ' \n' + + ' \n' + + ' Hello from Blockly!\n' + + ' \n' + + ' \n' + + ' \n' + + ''; + +const json = { + 'blocks': { + 'languageVersion': 0, + 'blocks': [ + { + 'type': 'procedures_defnoreturn', + 'id': '0!;|f{%4H@mgQ`SIEDKV', + 'x': 38, + 'y': 163, + 'icons': { + 'comment': { + 'text': 'Describe this function...', + 'pinned': false, + 'height': 80, + 'width': 160, + }, + }, + 'fields': { + 'NAME': 'say hello', + }, + 'inputs': { + 'STACK': { + 'block': { + 'type': 'text_print', + 'id': 't^`WoL~R$t}rk]`JVFUP', + 'inputs': { + 'TEXT': { + 'shadow': { + 'type': 'text', + 'id': '_PxHV1tqEy60kP^].Qhh', + 'fields': { + 'TEXT': 'abc', + }, + }, + 'block': { + 'type': 'text_join', + 'id': 'K4.OZ9ql9j0f367238R@', + 'extraState': { + 'itemCount': 2, + }, + 'inputs': { + 'ADD0': { + 'block': { + 'type': 'text', + 'id': '5ElufS^j4;l:9N#|Yt$X', + 'fields': { + 'TEXT': 'The meaning of life is', + }, + }, + }, + 'ADD1': { + 'block': { + 'type': 'math_arithmetic', + 'id': ',QfcN`h]rQ86a]6J|di1', + 'fields': { + 'OP': 'MINUS', + }, + 'inputs': { + 'A': { + 'shadow': { + 'type': 'math_number', + 'id': 'ClcKUIPYleVQ_j7ZjK]^', + 'fields': { + 'NUM': 44, + }, + }, + }, + 'B': { + 'shadow': { + 'type': 'math_number', + 'id': 'F_cU|uaP7oB-k(j~@X?g', + 'fields': { + 'NUM': 2, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + 'type': 'procedures_callnoreturn', + 'id': 'Ad^$sruQ.`6zNmQ6jPit', + 'x': 38, + 'y': 113, + 'extraState': { + 'name': 'say hello', + }, + }, + ], + }, +}; + +suite('Test Node.js', function () { + test('Import XML', function () { + const xml = Blockly.utils.xml.textToDom(xmlText); + + // Create workspace and import the XML + const workspace = new Blockly.Workspace(); + Blockly.Xml.domToWorkspace(xml, workspace); + }); + test('Roundtrip XML', function () { + const xml = Blockly.utils.xml.textToDom(xmlText); + + const workspace = new Blockly.Workspace(); + Blockly.Xml.domToWorkspace(xml, workspace); + + const headlessXml = Blockly.Xml.workspaceToDom(workspace, true); + const headlessText = Blockly.Xml.domToPrettyText(headlessXml); + + assert.equal(headlessText, xmlText, 'equal'); + }); + test('Generate Code', function () { + const xml = Blockly.utils.xml.textToDom(xmlText); + + // Create workspace and import the XML + const workspace = new Blockly.Workspace(); + Blockly.Xml.domToWorkspace(xml, workspace); + + // Convert code + const code = javascriptGenerator.workspaceToCode(workspace); + + // Check output + assert.equal("window.alert('Hello from Blockly!');", code.trim(), 'equal'); + }); + test('Import JSON', function () { + const workspace = new Blockly.Workspace(); + Blockly.serialization.workspaces.load(json, workspace); + }); + test('Roundtrip JSON', function () { + const workspace = new Blockly.Workspace(); + Blockly.serialization.workspaces.load(json, workspace); + + const jsonAfter = Blockly.serialization.workspaces.save(workspace); + + assert.deepEqual(jsonAfter, json); + }); + test('Dropdown getText works with no HTMLElement defined', function () { + const field = new Blockly.FieldDropdown([ + ['firstOption', '1'], + ['secondOption', '2'], + ]); + assert.equal(field.getText(), 'firstOption'); + }); +}); diff --git a/tests/playground.html b/packages/blockly/tests/playground.html similarity index 100% rename from tests/playground.html rename to packages/blockly/tests/playground.html diff --git a/tests/playgrounds/advanced_playground.html b/packages/blockly/tests/playgrounds/advanced_playground.html similarity index 81% rename from tests/playgrounds/advanced_playground.html rename to packages/blockly/tests/playgrounds/advanced_playground.html index 1fcbbd812a9..5c00de6ee99 100644 --- a/tests/playgrounds/advanced_playground.html +++ b/packages/blockly/tests/playgrounds/advanced_playground.html @@ -18,6 +18,11 @@ await loadScript( '../../node_modules/@blockly/theme-modern/dist/index.js', ); + await loadScript( + '../../node_modules/@blockly/keyboard-navigation/dist/index.js', + ); + + let kbNavigation; function start() { setBackgroundColour(); @@ -47,6 +52,28 @@ // Refresh theme. ws.setTheme(ws.getTheme()); }); + + // Keyboard navigation options. + const kbOptions = { + 'Enable keyboard navigation': false, + }; + gui.remember(kbOptions); + gui.add(kbOptions, 'Enable keyboard navigation').onChange((enabled) => { + setupKeyboardNav(enabled, playground); + }); + + // Set up keyboard navigation on page load + setupKeyboardNav(kbOptions['Enable keyboard navigation'], playground); + } + + function setupKeyboardNav(enabled, playground) { + if (enabled) { + kbNavigation = new KeyboardNavigation(playground.getWorkspace()); + } else { + if (kbNavigation) { + kbNavigation.dispose(); + } + } } function initPlayground() { @@ -101,6 +128,8 @@ }; Blockly.ContextMenuItems.registerCommentOptions(); + KeyboardNavigation.registerKeyboardNavigationStyles(); + // TODO: register the navigation-deferring toolbox. createPlayground( document.getElementById('root'), @@ -153,6 +182,7 @@ +
diff --git a/tests/playgrounds/iframe.html b/packages/blockly/tests/playgrounds/iframe.html similarity index 100% rename from tests/playgrounds/iframe.html rename to packages/blockly/tests/playgrounds/iframe.html diff --git a/tests/playgrounds/screenshot.js b/packages/blockly/tests/playgrounds/screenshot.js similarity index 100% rename from tests/playgrounds/screenshot.js rename to packages/blockly/tests/playgrounds/screenshot.js diff --git a/tests/scripts/check_metadata.sh b/packages/blockly/tests/scripts/check_metadata.sh similarity index 95% rename from tests/scripts/check_metadata.sh rename to packages/blockly/tests/scripts/check_metadata.sh index 5c4eb5dcc98..321c8ea03cd 100755 --- a/tests/scripts/check_metadata.sh +++ b/packages/blockly/tests/scripts/check_metadata.sh @@ -39,7 +39,8 @@ readonly RELEASE_DIR='dist' # Q4 2023 10.2.2 903535 # Q1 2024 10.3.1 914366 # Q2 2024 11.0.0 905365 -readonly BLOCKLY_SIZE_EXPECTED=905365 +# Q2 2025 11.2.2 922504 +readonly BLOCKLY_SIZE_EXPECTED=922504 # Size of blocks_compressed.js # Q2 2019 2.20190722.0 75618 @@ -64,7 +65,8 @@ readonly BLOCKLY_SIZE_EXPECTED=905365 # Q4 2023 10.2.2 90269 # Q1 2024 10.3.1 90269 # Q2 2024 11.0.0 88376 -readonly BLOCKS_SIZE_EXPECTED=88376 +# Q2 2025 11.2.2 88845 +readonly BLOCKS_SIZE_EXPECTED=88845 # Size of blockly_compressed.js.gz # Q2 2019 2.20190722.0 180925 @@ -90,7 +92,8 @@ readonly BLOCKS_SIZE_EXPECTED=88376 # Q4 2023 10.2.2 181474 # Q1 2024 10.3.1 184237 # Q2 2024 11.0.0 182249 -readonly BLOCKLY_GZ_SIZE_EXPECTED=182249 +# Q2 2025 11.2.2 185336 +readonly BLOCKLY_GZ_SIZE_EXPECTED=185336 # Size of blocks_compressed.js.gz # Q2 2019 2.20190722.0 14552 @@ -115,7 +118,8 @@ readonly BLOCKLY_GZ_SIZE_EXPECTED=182249 # Q4 2023 10.2.2 16442 # Q1 2024 10.3.1 16533 # Q2 2024 11.0.0 15815 -readonly BLOCKS_GZ_SIZE_EXPECTED=15815 +# Q2 2025 11.2.2 15887 +readonly BLOCKS_GZ_SIZE_EXPECTED=15887 # ANSI colors readonly BOLD_GREEN='\033[1;32m' diff --git a/tests/scripts/load.mjs b/packages/blockly/tests/scripts/load.mjs similarity index 100% rename from tests/scripts/load.mjs rename to packages/blockly/tests/scripts/load.mjs diff --git a/tests/scripts/setup_linux_env.sh b/packages/blockly/tests/scripts/setup_linux_env.sh similarity index 100% rename from tests/scripts/setup_linux_env.sh rename to packages/blockly/tests/scripts/setup_linux_env.sh diff --git a/tests/scripts/update_metadata.sh b/packages/blockly/tests/scripts/update_metadata.sh similarity index 100% rename from tests/scripts/update_metadata.sh rename to packages/blockly/tests/scripts/update_metadata.sh diff --git a/tests/themes/test_themes.js b/packages/blockly/tests/themes/test_themes.js similarity index 100% rename from tests/themes/test_themes.js rename to packages/blockly/tests/themes/test_themes.js diff --git a/tests/typescript/README.md b/packages/blockly/tests/typescript/README.md similarity index 100% rename from tests/typescript/README.md rename to packages/blockly/tests/typescript/README.md diff --git a/tests/typescript/src/field/different_user_input.ts b/packages/blockly/tests/typescript/src/field/different_user_input.ts similarity index 100% rename from tests/typescript/src/field/different_user_input.ts rename to packages/blockly/tests/typescript/src/field/different_user_input.ts diff --git a/tests/typescript/src/generators.ts b/packages/blockly/tests/typescript/src/generators.ts similarity index 100% rename from tests/typescript/src/generators.ts rename to packages/blockly/tests/typescript/src/generators.ts diff --git a/tests/typescript/src/generators/dart.ts b/packages/blockly/tests/typescript/src/generators/dart.ts similarity index 100% rename from tests/typescript/src/generators/dart.ts rename to packages/blockly/tests/typescript/src/generators/dart.ts diff --git a/tests/typescript/src/generators/javascript.ts b/packages/blockly/tests/typescript/src/generators/javascript.ts similarity index 100% rename from tests/typescript/src/generators/javascript.ts rename to packages/blockly/tests/typescript/src/generators/javascript.ts diff --git a/tests/typescript/src/generators/lua.ts b/packages/blockly/tests/typescript/src/generators/lua.ts similarity index 100% rename from tests/typescript/src/generators/lua.ts rename to packages/blockly/tests/typescript/src/generators/lua.ts diff --git a/tests/typescript/src/generators/php.ts b/packages/blockly/tests/typescript/src/generators/php.ts similarity index 100% rename from tests/typescript/src/generators/php.ts rename to packages/blockly/tests/typescript/src/generators/php.ts diff --git a/tests/typescript/src/generators/python.ts b/packages/blockly/tests/typescript/src/generators/python.ts similarity index 100% rename from tests/typescript/src/generators/python.ts rename to packages/blockly/tests/typescript/src/generators/python.ts diff --git a/tests/typescript/src/msg.ts b/packages/blockly/tests/typescript/src/msg.ts similarity index 100% rename from tests/typescript/src/msg.ts rename to packages/blockly/tests/typescript/src/msg.ts diff --git a/tests/typescript/tsconfig.json b/packages/blockly/tests/typescript/tsconfig.json similarity index 100% rename from tests/typescript/tsconfig.json rename to packages/blockly/tests/typescript/tsconfig.json diff --git a/tests/xml/README.txt b/packages/blockly/tests/xml/README.txt similarity index 100% rename from tests/xml/README.txt rename to packages/blockly/tests/xml/README.txt diff --git a/tests/xml/blockly.xsd b/packages/blockly/tests/xml/blockly.xsd similarity index 100% rename from tests/xml/blockly.xsd rename to packages/blockly/tests/xml/blockly.xsd diff --git a/tests/xml/invalid.xml b/packages/blockly/tests/xml/invalid.xml similarity index 100% rename from tests/xml/invalid.xml rename to packages/blockly/tests/xml/invalid.xml diff --git a/tests/xml/toolbox.xml b/packages/blockly/tests/xml/toolbox.xml similarity index 100% rename from tests/xml/toolbox.xml rename to packages/blockly/tests/xml/toolbox.xml diff --git a/tests/xml/workspace.xml b/packages/blockly/tests/xml/workspace.xml similarity index 100% rename from tests/xml/workspace.xml rename to packages/blockly/tests/xml/workspace.xml diff --git a/tsconfig.json b/packages/blockly/tsconfig.json similarity index 100% rename from tsconfig.json rename to packages/blockly/tsconfig.json diff --git a/tsdoc.json b/packages/blockly/tsdoc.json similarity index 71% rename from tsdoc.json rename to packages/blockly/tsdoc.json index 51900e7c369..5470830e746 100644 --- a/tsdoc.json +++ b/packages/blockly/tsdoc.json @@ -3,10 +3,6 @@ // Include the definitions that are required for API Extractor "extends": ["@microsoft/api-extractor/extends/tsdoc-base.json"], "tagDefinitions": [ - { - "tagName": "@alias", - "syntaxKind": "block" - }, { "tagName": "@define", "syntaxKind": "block" @@ -18,18 +14,12 @@ { "tagName": "@nocollapse", "syntaxKind": "modifier" - }, - { - "tagName": "@suppress", - "syntaxKind": "modifier" } ], "supportForTags": { - "@alias": true, "@define": true, "@license": true, - "@nocollapse": true, - "@suppress": true + "@nocollapse": true } } diff --git a/typings/README.md b/packages/blockly/typings/README.md similarity index 100% rename from typings/README.md rename to packages/blockly/typings/README.md diff --git a/typings/blocks.d.ts b/packages/blockly/typings/blocks.d.ts similarity index 100% rename from typings/blocks.d.ts rename to packages/blockly/typings/blocks.d.ts diff --git a/typings/core.d.ts b/packages/blockly/typings/core.d.ts similarity index 100% rename from typings/core.d.ts rename to packages/blockly/typings/core.d.ts diff --git a/typings/dart.d.ts b/packages/blockly/typings/dart.d.ts similarity index 100% rename from typings/dart.d.ts rename to packages/blockly/typings/dart.d.ts diff --git a/typings/index.d.ts b/packages/blockly/typings/index.d.ts similarity index 100% rename from typings/index.d.ts rename to packages/blockly/typings/index.d.ts diff --git a/typings/javascript.d.ts b/packages/blockly/typings/javascript.d.ts similarity index 100% rename from typings/javascript.d.ts rename to packages/blockly/typings/javascript.d.ts diff --git a/typings/lua.d.ts b/packages/blockly/typings/lua.d.ts similarity index 100% rename from typings/lua.d.ts rename to packages/blockly/typings/lua.d.ts diff --git a/typings/msg/ab.d.ts b/packages/blockly/typings/msg/ab.d.ts similarity index 100% rename from typings/msg/ab.d.ts rename to packages/blockly/typings/msg/ab.d.ts diff --git a/typings/msg/ace.d.ts b/packages/blockly/typings/msg/ace.d.ts similarity index 100% rename from typings/msg/ace.d.ts rename to packages/blockly/typings/msg/ace.d.ts diff --git a/typings/msg/af.d.ts b/packages/blockly/typings/msg/af.d.ts similarity index 100% rename from typings/msg/af.d.ts rename to packages/blockly/typings/msg/af.d.ts diff --git a/typings/msg/am.d.ts b/packages/blockly/typings/msg/am.d.ts similarity index 100% rename from typings/msg/am.d.ts rename to packages/blockly/typings/msg/am.d.ts diff --git a/typings/msg/ar.d.ts b/packages/blockly/typings/msg/ar.d.ts similarity index 100% rename from typings/msg/ar.d.ts rename to packages/blockly/typings/msg/ar.d.ts diff --git a/typings/msg/ast.d.ts b/packages/blockly/typings/msg/ast.d.ts similarity index 100% rename from typings/msg/ast.d.ts rename to packages/blockly/typings/msg/ast.d.ts diff --git a/typings/msg/az.d.ts b/packages/blockly/typings/msg/az.d.ts similarity index 100% rename from typings/msg/az.d.ts rename to packages/blockly/typings/msg/az.d.ts diff --git a/typings/msg/ba.d.ts b/packages/blockly/typings/msg/ba.d.ts similarity index 100% rename from typings/msg/ba.d.ts rename to packages/blockly/typings/msg/ba.d.ts diff --git a/typings/msg/bcc.d.ts b/packages/blockly/typings/msg/bcc.d.ts similarity index 100% rename from typings/msg/bcc.d.ts rename to packages/blockly/typings/msg/bcc.d.ts diff --git a/typings/msg/be-tarask.d.ts b/packages/blockly/typings/msg/be-tarask.d.ts similarity index 100% rename from typings/msg/be-tarask.d.ts rename to packages/blockly/typings/msg/be-tarask.d.ts diff --git a/typings/msg/be.d.ts b/packages/blockly/typings/msg/be.d.ts similarity index 100% rename from typings/msg/be.d.ts rename to packages/blockly/typings/msg/be.d.ts diff --git a/typings/msg/bg.d.ts b/packages/blockly/typings/msg/bg.d.ts similarity index 100% rename from typings/msg/bg.d.ts rename to packages/blockly/typings/msg/bg.d.ts diff --git a/typings/msg/bn.d.ts b/packages/blockly/typings/msg/bn.d.ts similarity index 100% rename from typings/msg/bn.d.ts rename to packages/blockly/typings/msg/bn.d.ts diff --git a/typings/msg/br.d.ts b/packages/blockly/typings/msg/br.d.ts similarity index 100% rename from typings/msg/br.d.ts rename to packages/blockly/typings/msg/br.d.ts diff --git a/typings/msg/bs.d.ts b/packages/blockly/typings/msg/bs.d.ts similarity index 100% rename from typings/msg/bs.d.ts rename to packages/blockly/typings/msg/bs.d.ts diff --git a/typings/msg/ca.d.ts b/packages/blockly/typings/msg/ca.d.ts similarity index 100% rename from typings/msg/ca.d.ts rename to packages/blockly/typings/msg/ca.d.ts diff --git a/typings/msg/cdo.d.ts b/packages/blockly/typings/msg/cdo.d.ts similarity index 100% rename from typings/msg/cdo.d.ts rename to packages/blockly/typings/msg/cdo.d.ts diff --git a/typings/msg/ce.d.ts b/packages/blockly/typings/msg/ce.d.ts similarity index 100% rename from typings/msg/ce.d.ts rename to packages/blockly/typings/msg/ce.d.ts diff --git a/typings/msg/cs.d.ts b/packages/blockly/typings/msg/cs.d.ts similarity index 100% rename from typings/msg/cs.d.ts rename to packages/blockly/typings/msg/cs.d.ts diff --git a/typings/msg/da.d.ts b/packages/blockly/typings/msg/da.d.ts similarity index 100% rename from typings/msg/da.d.ts rename to packages/blockly/typings/msg/da.d.ts diff --git a/typings/msg/de.d.ts b/packages/blockly/typings/msg/de.d.ts similarity index 100% rename from typings/msg/de.d.ts rename to packages/blockly/typings/msg/de.d.ts diff --git a/typings/msg/diq.d.ts b/packages/blockly/typings/msg/diq.d.ts similarity index 100% rename from typings/msg/diq.d.ts rename to packages/blockly/typings/msg/diq.d.ts diff --git a/typings/msg/dtp.d.ts b/packages/blockly/typings/msg/dtp.d.ts similarity index 100% rename from typings/msg/dtp.d.ts rename to packages/blockly/typings/msg/dtp.d.ts diff --git a/typings/msg/dty.d.ts b/packages/blockly/typings/msg/dty.d.ts similarity index 100% rename from typings/msg/dty.d.ts rename to packages/blockly/typings/msg/dty.d.ts diff --git a/typings/msg/ee.d.ts b/packages/blockly/typings/msg/ee.d.ts similarity index 100% rename from typings/msg/ee.d.ts rename to packages/blockly/typings/msg/ee.d.ts diff --git a/typings/msg/el.d.ts b/packages/blockly/typings/msg/el.d.ts similarity index 100% rename from typings/msg/el.d.ts rename to packages/blockly/typings/msg/el.d.ts diff --git a/typings/msg/en-gb.d.ts b/packages/blockly/typings/msg/en-gb.d.ts similarity index 100% rename from typings/msg/en-gb.d.ts rename to packages/blockly/typings/msg/en-gb.d.ts diff --git a/typings/msg/en.d.ts b/packages/blockly/typings/msg/en.d.ts similarity index 100% rename from typings/msg/en.d.ts rename to packages/blockly/typings/msg/en.d.ts diff --git a/typings/msg/eo.d.ts b/packages/blockly/typings/msg/eo.d.ts similarity index 100% rename from typings/msg/eo.d.ts rename to packages/blockly/typings/msg/eo.d.ts diff --git a/typings/msg/es.d.ts b/packages/blockly/typings/msg/es.d.ts similarity index 100% rename from typings/msg/es.d.ts rename to packages/blockly/typings/msg/es.d.ts diff --git a/typings/msg/et.d.ts b/packages/blockly/typings/msg/et.d.ts similarity index 100% rename from typings/msg/et.d.ts rename to packages/blockly/typings/msg/et.d.ts diff --git a/typings/msg/eu.d.ts b/packages/blockly/typings/msg/eu.d.ts similarity index 100% rename from typings/msg/eu.d.ts rename to packages/blockly/typings/msg/eu.d.ts diff --git a/typings/msg/fa.d.ts b/packages/blockly/typings/msg/fa.d.ts similarity index 100% rename from typings/msg/fa.d.ts rename to packages/blockly/typings/msg/fa.d.ts diff --git a/typings/msg/fi.d.ts b/packages/blockly/typings/msg/fi.d.ts similarity index 100% rename from typings/msg/fi.d.ts rename to packages/blockly/typings/msg/fi.d.ts diff --git a/typings/msg/fo.d.ts b/packages/blockly/typings/msg/fo.d.ts similarity index 100% rename from typings/msg/fo.d.ts rename to packages/blockly/typings/msg/fo.d.ts diff --git a/typings/msg/fr.d.ts b/packages/blockly/typings/msg/fr.d.ts similarity index 100% rename from typings/msg/fr.d.ts rename to packages/blockly/typings/msg/fr.d.ts diff --git a/typings/msg/frr.d.ts b/packages/blockly/typings/msg/frr.d.ts similarity index 100% rename from typings/msg/frr.d.ts rename to packages/blockly/typings/msg/frr.d.ts diff --git a/typings/msg/gl.d.ts b/packages/blockly/typings/msg/gl.d.ts similarity index 100% rename from typings/msg/gl.d.ts rename to packages/blockly/typings/msg/gl.d.ts diff --git a/typings/msg/gn.d.ts b/packages/blockly/typings/msg/gn.d.ts similarity index 100% rename from typings/msg/gn.d.ts rename to packages/blockly/typings/msg/gn.d.ts diff --git a/typings/msg/gor.d.ts b/packages/blockly/typings/msg/gor.d.ts similarity index 100% rename from typings/msg/gor.d.ts rename to packages/blockly/typings/msg/gor.d.ts diff --git a/typings/msg/ha.d.ts b/packages/blockly/typings/msg/ha.d.ts similarity index 100% rename from typings/msg/ha.d.ts rename to packages/blockly/typings/msg/ha.d.ts diff --git a/typings/msg/hak.d.ts b/packages/blockly/typings/msg/hak.d.ts similarity index 100% rename from typings/msg/hak.d.ts rename to packages/blockly/typings/msg/hak.d.ts diff --git a/typings/msg/he.d.ts b/packages/blockly/typings/msg/he.d.ts similarity index 100% rename from typings/msg/he.d.ts rename to packages/blockly/typings/msg/he.d.ts diff --git a/typings/msg/hi.d.ts b/packages/blockly/typings/msg/hi.d.ts similarity index 100% rename from typings/msg/hi.d.ts rename to packages/blockly/typings/msg/hi.d.ts diff --git a/typings/msg/hr.d.ts b/packages/blockly/typings/msg/hr.d.ts similarity index 100% rename from typings/msg/hr.d.ts rename to packages/blockly/typings/msg/hr.d.ts diff --git a/typings/msg/hrx.d.ts b/packages/blockly/typings/msg/hrx.d.ts similarity index 100% rename from typings/msg/hrx.d.ts rename to packages/blockly/typings/msg/hrx.d.ts diff --git a/typings/msg/hsb.d.ts b/packages/blockly/typings/msg/hsb.d.ts similarity index 100% rename from typings/msg/hsb.d.ts rename to packages/blockly/typings/msg/hsb.d.ts diff --git a/typings/msg/hu.d.ts b/packages/blockly/typings/msg/hu.d.ts similarity index 100% rename from typings/msg/hu.d.ts rename to packages/blockly/typings/msg/hu.d.ts diff --git a/typings/msg/hy.d.ts b/packages/blockly/typings/msg/hy.d.ts similarity index 100% rename from typings/msg/hy.d.ts rename to packages/blockly/typings/msg/hy.d.ts diff --git a/typings/msg/ia.d.ts b/packages/blockly/typings/msg/ia.d.ts similarity index 100% rename from typings/msg/ia.d.ts rename to packages/blockly/typings/msg/ia.d.ts diff --git a/typings/msg/id.d.ts b/packages/blockly/typings/msg/id.d.ts similarity index 100% rename from typings/msg/id.d.ts rename to packages/blockly/typings/msg/id.d.ts diff --git a/typings/msg/ig.d.ts b/packages/blockly/typings/msg/ig.d.ts similarity index 100% rename from typings/msg/ig.d.ts rename to packages/blockly/typings/msg/ig.d.ts diff --git a/typings/msg/inh.d.ts b/packages/blockly/typings/msg/inh.d.ts similarity index 100% rename from typings/msg/inh.d.ts rename to packages/blockly/typings/msg/inh.d.ts diff --git a/typings/msg/is.d.ts b/packages/blockly/typings/msg/is.d.ts similarity index 100% rename from typings/msg/is.d.ts rename to packages/blockly/typings/msg/is.d.ts diff --git a/typings/msg/it.d.ts b/packages/blockly/typings/msg/it.d.ts similarity index 100% rename from typings/msg/it.d.ts rename to packages/blockly/typings/msg/it.d.ts diff --git a/typings/msg/ja.d.ts b/packages/blockly/typings/msg/ja.d.ts similarity index 100% rename from typings/msg/ja.d.ts rename to packages/blockly/typings/msg/ja.d.ts diff --git a/typings/msg/ka.d.ts b/packages/blockly/typings/msg/ka.d.ts similarity index 100% rename from typings/msg/ka.d.ts rename to packages/blockly/typings/msg/ka.d.ts diff --git a/typings/msg/kab.d.ts b/packages/blockly/typings/msg/kab.d.ts similarity index 100% rename from typings/msg/kab.d.ts rename to packages/blockly/typings/msg/kab.d.ts diff --git a/typings/msg/kbd-cyrl.d.ts b/packages/blockly/typings/msg/kbd-cyrl.d.ts similarity index 100% rename from typings/msg/kbd-cyrl.d.ts rename to packages/blockly/typings/msg/kbd-cyrl.d.ts diff --git a/typings/msg/km.d.ts b/packages/blockly/typings/msg/km.d.ts similarity index 100% rename from typings/msg/km.d.ts rename to packages/blockly/typings/msg/km.d.ts diff --git a/typings/msg/kn.d.ts b/packages/blockly/typings/msg/kn.d.ts similarity index 100% rename from typings/msg/kn.d.ts rename to packages/blockly/typings/msg/kn.d.ts diff --git a/typings/msg/ko.d.ts b/packages/blockly/typings/msg/ko.d.ts similarity index 100% rename from typings/msg/ko.d.ts rename to packages/blockly/typings/msg/ko.d.ts diff --git a/typings/msg/ksh.d.ts b/packages/blockly/typings/msg/ksh.d.ts similarity index 100% rename from typings/msg/ksh.d.ts rename to packages/blockly/typings/msg/ksh.d.ts diff --git a/typings/msg/ku-latn.d.ts b/packages/blockly/typings/msg/ku-latn.d.ts similarity index 100% rename from typings/msg/ku-latn.d.ts rename to packages/blockly/typings/msg/ku-latn.d.ts diff --git a/typings/msg/ky.d.ts b/packages/blockly/typings/msg/ky.d.ts similarity index 100% rename from typings/msg/ky.d.ts rename to packages/blockly/typings/msg/ky.d.ts diff --git a/typings/msg/la.d.ts b/packages/blockly/typings/msg/la.d.ts similarity index 100% rename from typings/msg/la.d.ts rename to packages/blockly/typings/msg/la.d.ts diff --git a/typings/msg/lb.d.ts b/packages/blockly/typings/msg/lb.d.ts similarity index 100% rename from typings/msg/lb.d.ts rename to packages/blockly/typings/msg/lb.d.ts diff --git a/typings/msg/lki.d.ts b/packages/blockly/typings/msg/lki.d.ts similarity index 100% rename from typings/msg/lki.d.ts rename to packages/blockly/typings/msg/lki.d.ts diff --git a/typings/msg/lo.d.ts b/packages/blockly/typings/msg/lo.d.ts similarity index 100% rename from typings/msg/lo.d.ts rename to packages/blockly/typings/msg/lo.d.ts diff --git a/typings/msg/lrc.d.ts b/packages/blockly/typings/msg/lrc.d.ts similarity index 100% rename from typings/msg/lrc.d.ts rename to packages/blockly/typings/msg/lrc.d.ts diff --git a/typings/msg/lt.d.ts b/packages/blockly/typings/msg/lt.d.ts similarity index 100% rename from typings/msg/lt.d.ts rename to packages/blockly/typings/msg/lt.d.ts diff --git a/typings/msg/lv.d.ts b/packages/blockly/typings/msg/lv.d.ts similarity index 100% rename from typings/msg/lv.d.ts rename to packages/blockly/typings/msg/lv.d.ts diff --git a/typings/msg/mg.d.ts b/packages/blockly/typings/msg/mg.d.ts similarity index 100% rename from typings/msg/mg.d.ts rename to packages/blockly/typings/msg/mg.d.ts diff --git a/typings/msg/mk.d.ts b/packages/blockly/typings/msg/mk.d.ts similarity index 100% rename from typings/msg/mk.d.ts rename to packages/blockly/typings/msg/mk.d.ts diff --git a/typings/msg/ml.d.ts b/packages/blockly/typings/msg/ml.d.ts similarity index 100% rename from typings/msg/ml.d.ts rename to packages/blockly/typings/msg/ml.d.ts diff --git a/typings/msg/mnw.d.ts b/packages/blockly/typings/msg/mnw.d.ts similarity index 100% rename from typings/msg/mnw.d.ts rename to packages/blockly/typings/msg/mnw.d.ts diff --git a/typings/msg/ms.d.ts b/packages/blockly/typings/msg/ms.d.ts similarity index 100% rename from typings/msg/ms.d.ts rename to packages/blockly/typings/msg/ms.d.ts diff --git a/typings/msg/msg.d.ts b/packages/blockly/typings/msg/msg.d.ts similarity index 100% rename from typings/msg/msg.d.ts rename to packages/blockly/typings/msg/msg.d.ts diff --git a/typings/msg/my.d.ts b/packages/blockly/typings/msg/my.d.ts similarity index 100% rename from typings/msg/my.d.ts rename to packages/blockly/typings/msg/my.d.ts diff --git a/typings/msg/mzn.d.ts b/packages/blockly/typings/msg/mzn.d.ts similarity index 100% rename from typings/msg/mzn.d.ts rename to packages/blockly/typings/msg/mzn.d.ts diff --git a/typings/msg/nb.d.ts b/packages/blockly/typings/msg/nb.d.ts similarity index 100% rename from typings/msg/nb.d.ts rename to packages/blockly/typings/msg/nb.d.ts diff --git a/typings/msg/ne.d.ts b/packages/blockly/typings/msg/ne.d.ts similarity index 100% rename from typings/msg/ne.d.ts rename to packages/blockly/typings/msg/ne.d.ts diff --git a/typings/msg/nl.d.ts b/packages/blockly/typings/msg/nl.d.ts similarity index 100% rename from typings/msg/nl.d.ts rename to packages/blockly/typings/msg/nl.d.ts diff --git a/typings/msg/oc.d.ts b/packages/blockly/typings/msg/oc.d.ts similarity index 100% rename from typings/msg/oc.d.ts rename to packages/blockly/typings/msg/oc.d.ts diff --git a/typings/msg/olo.d.ts b/packages/blockly/typings/msg/olo.d.ts similarity index 100% rename from typings/msg/olo.d.ts rename to packages/blockly/typings/msg/olo.d.ts diff --git a/typings/msg/pa.d.ts b/packages/blockly/typings/msg/pa.d.ts similarity index 100% rename from typings/msg/pa.d.ts rename to packages/blockly/typings/msg/pa.d.ts diff --git a/typings/msg/pl.d.ts b/packages/blockly/typings/msg/pl.d.ts similarity index 100% rename from typings/msg/pl.d.ts rename to packages/blockly/typings/msg/pl.d.ts diff --git a/typings/msg/pms.d.ts b/packages/blockly/typings/msg/pms.d.ts similarity index 100% rename from typings/msg/pms.d.ts rename to packages/blockly/typings/msg/pms.d.ts diff --git a/typings/msg/ps.d.ts b/packages/blockly/typings/msg/ps.d.ts similarity index 100% rename from typings/msg/ps.d.ts rename to packages/blockly/typings/msg/ps.d.ts diff --git a/typings/msg/pt-br.d.ts b/packages/blockly/typings/msg/pt-br.d.ts similarity index 100% rename from typings/msg/pt-br.d.ts rename to packages/blockly/typings/msg/pt-br.d.ts diff --git a/typings/msg/pt.d.ts b/packages/blockly/typings/msg/pt.d.ts similarity index 100% rename from typings/msg/pt.d.ts rename to packages/blockly/typings/msg/pt.d.ts diff --git a/typings/msg/ro.d.ts b/packages/blockly/typings/msg/ro.d.ts similarity index 100% rename from typings/msg/ro.d.ts rename to packages/blockly/typings/msg/ro.d.ts diff --git a/typings/msg/ru.d.ts b/packages/blockly/typings/msg/ru.d.ts similarity index 100% rename from typings/msg/ru.d.ts rename to packages/blockly/typings/msg/ru.d.ts diff --git a/typings/msg/sc.d.ts b/packages/blockly/typings/msg/sc.d.ts similarity index 100% rename from typings/msg/sc.d.ts rename to packages/blockly/typings/msg/sc.d.ts diff --git a/typings/msg/sco.d.ts b/packages/blockly/typings/msg/sco.d.ts similarity index 100% rename from typings/msg/sco.d.ts rename to packages/blockly/typings/msg/sco.d.ts diff --git a/typings/msg/sd.d.ts b/packages/blockly/typings/msg/sd.d.ts similarity index 100% rename from typings/msg/sd.d.ts rename to packages/blockly/typings/msg/sd.d.ts diff --git a/typings/msg/shn.d.ts b/packages/blockly/typings/msg/shn.d.ts similarity index 100% rename from typings/msg/shn.d.ts rename to packages/blockly/typings/msg/shn.d.ts diff --git a/typings/msg/si.d.ts b/packages/blockly/typings/msg/si.d.ts similarity index 100% rename from typings/msg/si.d.ts rename to packages/blockly/typings/msg/si.d.ts diff --git a/typings/msg/sk.d.ts b/packages/blockly/typings/msg/sk.d.ts similarity index 100% rename from typings/msg/sk.d.ts rename to packages/blockly/typings/msg/sk.d.ts diff --git a/typings/msg/skr-arab.d.ts b/packages/blockly/typings/msg/skr-arab.d.ts similarity index 100% rename from typings/msg/skr-arab.d.ts rename to packages/blockly/typings/msg/skr-arab.d.ts diff --git a/typings/msg/sl.d.ts b/packages/blockly/typings/msg/sl.d.ts similarity index 100% rename from typings/msg/sl.d.ts rename to packages/blockly/typings/msg/sl.d.ts diff --git a/typings/msg/smn.d.ts b/packages/blockly/typings/msg/smn.d.ts similarity index 100% rename from typings/msg/smn.d.ts rename to packages/blockly/typings/msg/smn.d.ts diff --git a/typings/msg/sq.d.ts b/packages/blockly/typings/msg/sq.d.ts similarity index 100% rename from typings/msg/sq.d.ts rename to packages/blockly/typings/msg/sq.d.ts diff --git a/typings/msg/sr-latn.d.ts b/packages/blockly/typings/msg/sr-latn.d.ts similarity index 100% rename from typings/msg/sr-latn.d.ts rename to packages/blockly/typings/msg/sr-latn.d.ts diff --git a/typings/msg/sr.d.ts b/packages/blockly/typings/msg/sr.d.ts similarity index 100% rename from typings/msg/sr.d.ts rename to packages/blockly/typings/msg/sr.d.ts diff --git a/typings/msg/sv.d.ts b/packages/blockly/typings/msg/sv.d.ts similarity index 100% rename from typings/msg/sv.d.ts rename to packages/blockly/typings/msg/sv.d.ts diff --git a/typings/msg/sw.d.ts b/packages/blockly/typings/msg/sw.d.ts similarity index 100% rename from typings/msg/sw.d.ts rename to packages/blockly/typings/msg/sw.d.ts diff --git a/typings/msg/ta.d.ts b/packages/blockly/typings/msg/ta.d.ts similarity index 100% rename from typings/msg/ta.d.ts rename to packages/blockly/typings/msg/ta.d.ts diff --git a/typings/msg/tcy.d.ts b/packages/blockly/typings/msg/tcy.d.ts similarity index 100% rename from typings/msg/tcy.d.ts rename to packages/blockly/typings/msg/tcy.d.ts diff --git a/typings/msg/tdd.d.ts b/packages/blockly/typings/msg/tdd.d.ts similarity index 100% rename from typings/msg/tdd.d.ts rename to packages/blockly/typings/msg/tdd.d.ts diff --git a/typings/msg/te.d.ts b/packages/blockly/typings/msg/te.d.ts similarity index 100% rename from typings/msg/te.d.ts rename to packages/blockly/typings/msg/te.d.ts diff --git a/typings/msg/th.d.ts b/packages/blockly/typings/msg/th.d.ts similarity index 100% rename from typings/msg/th.d.ts rename to packages/blockly/typings/msg/th.d.ts diff --git a/typings/msg/ti.d.ts b/packages/blockly/typings/msg/ti.d.ts similarity index 100% rename from typings/msg/ti.d.ts rename to packages/blockly/typings/msg/ti.d.ts diff --git a/typings/msg/tl.d.ts b/packages/blockly/typings/msg/tl.d.ts similarity index 100% rename from typings/msg/tl.d.ts rename to packages/blockly/typings/msg/tl.d.ts diff --git a/typings/msg/tlh.d.ts b/packages/blockly/typings/msg/tlh.d.ts similarity index 100% rename from typings/msg/tlh.d.ts rename to packages/blockly/typings/msg/tlh.d.ts diff --git a/typings/msg/tr.d.ts b/packages/blockly/typings/msg/tr.d.ts similarity index 100% rename from typings/msg/tr.d.ts rename to packages/blockly/typings/msg/tr.d.ts diff --git a/typings/msg/ug-arab.d.ts b/packages/blockly/typings/msg/ug-arab.d.ts similarity index 100% rename from typings/msg/ug-arab.d.ts rename to packages/blockly/typings/msg/ug-arab.d.ts diff --git a/typings/msg/uk.d.ts b/packages/blockly/typings/msg/uk.d.ts similarity index 100% rename from typings/msg/uk.d.ts rename to packages/blockly/typings/msg/uk.d.ts diff --git a/typings/msg/ur.d.ts b/packages/blockly/typings/msg/ur.d.ts similarity index 100% rename from typings/msg/ur.d.ts rename to packages/blockly/typings/msg/ur.d.ts diff --git a/typings/msg/uz.d.ts b/packages/blockly/typings/msg/uz.d.ts similarity index 100% rename from typings/msg/uz.d.ts rename to packages/blockly/typings/msg/uz.d.ts diff --git a/typings/msg/vi.d.ts b/packages/blockly/typings/msg/vi.d.ts similarity index 100% rename from typings/msg/vi.d.ts rename to packages/blockly/typings/msg/vi.d.ts diff --git a/typings/msg/xmf.d.ts b/packages/blockly/typings/msg/xmf.d.ts similarity index 100% rename from typings/msg/xmf.d.ts rename to packages/blockly/typings/msg/xmf.d.ts diff --git a/typings/msg/yo.d.ts b/packages/blockly/typings/msg/yo.d.ts similarity index 100% rename from typings/msg/yo.d.ts rename to packages/blockly/typings/msg/yo.d.ts diff --git a/typings/msg/zgh.d.ts b/packages/blockly/typings/msg/zgh.d.ts similarity index 100% rename from typings/msg/zgh.d.ts rename to packages/blockly/typings/msg/zgh.d.ts diff --git a/typings/msg/zh-hans.d.ts b/packages/blockly/typings/msg/zh-hans.d.ts similarity index 100% rename from typings/msg/zh-hans.d.ts rename to packages/blockly/typings/msg/zh-hans.d.ts diff --git a/typings/msg/zh-hant.d.ts b/packages/blockly/typings/msg/zh-hant.d.ts similarity index 100% rename from typings/msg/zh-hant.d.ts rename to packages/blockly/typings/msg/zh-hant.d.ts diff --git a/typings/php.d.ts b/packages/blockly/typings/php.d.ts similarity index 100% rename from typings/php.d.ts rename to packages/blockly/typings/php.d.ts diff --git a/typings/python.d.ts b/packages/blockly/typings/python.d.ts similarity index 100% rename from typings/python.d.ts rename to packages/blockly/typings/python.d.ts diff --git a/typings/templates/blockly-header.template b/packages/blockly/typings/templates/blockly-header.template similarity index 100% rename from typings/templates/blockly-header.template rename to packages/blockly/typings/templates/blockly-header.template diff --git a/typings/templates/blockly-interfaces.template b/packages/blockly/typings/templates/blockly-interfaces.template similarity index 100% rename from typings/templates/blockly-interfaces.template rename to packages/blockly/typings/templates/blockly-interfaces.template diff --git a/typings/templates/msg.template b/packages/blockly/typings/templates/msg.template similarity index 100% rename from typings/templates/msg.template rename to packages/blockly/typings/templates/msg.template diff --git a/typings/tsconfig.json b/packages/blockly/typings/tsconfig.json similarity index 100% rename from typings/tsconfig.json rename to packages/blockly/typings/tsconfig.json diff --git a/patches/@microsoft+api-documenter+7.22.4.patch b/patches/@microsoft+api-documenter+7.22.4.patch index 6e039a391e8..3cc97035da6 100644 --- a/patches/@microsoft+api-documenter+7.22.4.patch +++ b/patches/@microsoft+api-documenter+7.22.4.patch @@ -1,8 +1,8 @@ diff --git a/node_modules/@microsoft/api-documenter/lib/documenters/MarkdownDocumenter.js b/node_modules/@microsoft/api-documenter/lib/documenters/MarkdownDocumenter.js -index 5284d10..f2f9d14 100644 +index 5284d10..4f8b439 100644 --- a/node_modules/@microsoft/api-documenter/lib/documenters/MarkdownDocumenter.js +++ b/node_modules/@microsoft/api-documenter/lib/documenters/MarkdownDocumenter.js -@@ -877,12 +877,15 @@ class MarkdownDocumenter { +@@ -877,12 +877,14 @@ class MarkdownDocumenter { } _writeBreadcrumb(output, apiItem) { const configuration = this._tsdocConfiguration; @@ -19,28 +19,23 @@ index 5284d10..f2f9d14 100644 + // linkText: 'Home', + // urlDestination: this._getLinkFilenameForApiItem(this._apiModel) + // })); -+ + let first = true; for (const hierarchyItem of apiItem.getHierarchy()) { switch (hierarchyItem.kind) { case api_extractor_model_1.ApiItemKind.Model: -@@ -892,18 +895,24 @@ class MarkdownDocumenter { +@@ -892,18 +894,23 @@ class MarkdownDocumenter { // this may change in the future. break; default: - output.appendNodesInParagraph([ -- new tsdoc_1.DocPlainText({ -- configuration, -- text: ' > ' -- }), + if (!first) { + // Only print the breadcrumb separator if it's not the first item we're printing. + output.appendNodeInParagraph( -+ new tsdoc_1.DocPlainText({ -+ configuration, -+ text: ' > ' -+ }) -+ ); + new tsdoc_1.DocPlainText({ + configuration, + text: ' > ' +- }), ++ })); + } + first = false; + output.appendNodeInParagraph( @@ -55,7 +50,7 @@ index 5284d10..f2f9d14 100644 } } } -@@ -968,11 +977,8 @@ class MarkdownDocumenter { +@@ -968,11 +975,8 @@ class MarkdownDocumenter { // For overloaded methods, add a suffix such as "MyClass.myMethod_2". let qualifiedName = Utilities_1.Utilities.getSafeFilenameForName(hierarchyItem.displayName); if (api_extractor_model_1.ApiParameterListMixin.isBaseClassOf(hierarchyItem)) { @@ -69,7 +64,7 @@ index 5284d10..f2f9d14 100644 } switch (hierarchyItem.kind) { case api_extractor_model_1.ApiItemKind.Model: -@@ -983,7 +989,8 @@ class MarkdownDocumenter { +@@ -983,7 +987,8 @@ class MarkdownDocumenter { baseName = Utilities_1.Utilities.getSafeFilenameForName(node_core_library_1.PackageName.getUnscopedName(hierarchyItem.displayName)); break; default: diff --git a/sample.svg b/sample.svg new file mode 100644 index 00000000000..e83d469711c --- /dev/null +++ b/sample.svg @@ -0,0 +1 @@ +Count256‏>0Game OverprintsetCounttoifdo \ No newline at end of file diff --git a/scripts/goog_module/convert-file.sh b/scripts/goog_module/convert-file.sh deleted file mode 100755 index 4d6c1334523..00000000000 --- a/scripts/goog_module/convert-file.sh +++ /dev/null @@ -1,406 +0,0 @@ -#!/bin/bash - -# This file makes extensive use of perl for the purpose of extracting and -# replacing string (regex) patterns in a way that is both GNU and macOS -# compatible. -# -# Common perl flags used (also described at https://perldoc.perl.org/perlrun): -# -e : Used to execute perl programs on the command line -# -p : Assumes an input loop around script. Prints every processed line. -# -n : Assumes an input loop around script. Does not print every line. -# -i : Used for in-place editing. Used in commands for find/replace. -# -l[octnum] : Assigns the output record separator "$/" as an octal number. If -# octnum is not present, sets output record separator to the current -# value of the input record separator "$\". -# -# Common perl commands found: -# 1. perl -pi -e 's/regex/replacement/modifiers' -# This command does an in-place search-and-replace. The global ("/g") modifier -# causes it to replace all occurrences, rather than only the first match. -# 2. perl -ne 'print m/regex/modifiers' -# This command returns a string containing the regex match (designated by the -# capture group "()" in the regex). This will return the first match, unless -# the global modifier is specified, in which case, it will return all matches. -# If this command is used without a capture group it returns true or false (in -# the form a truthy or falsy value). -# 3. perl -nle 'print $& while m{regex}modifiers' -# Similar to (2), but returns regex matches separated by newlines. -# The "m{regex}modifiers" is equivalent to "m/regex/modifiers" syntax. -# -# Additional information on regex: -# This script makes use of some advanced regex syntax such as "capture groups" -# and "lookaround assertions". -# Additionally, characters are escaped from regex with a backslash "\". -# Single quotes need to be escaped in both regex and the string, resulting in -# '\'' being used to represent a single quote character. -# For a reference to syntax of regular expressions in Perl, see: -# https://perldoc.perl.org/perlre - -####################################### -# Logging functions. -####################################### -COLOR_NONE="\033[0m" -GREEN="\033[0;32m" -BLUE="\033[0;34m" -ORANGE="\033[0;33m" -RED="\033[0;31m" -success() { - echo -e "${GREEN}[SUCCESS]:${COLOR_NONE} $*" >&2 -} -inf() { - echo -e "${BLUE}[INFO]:${COLOR_NONE} $*" >&2 -} -warn() { - echo -e "${ORANGE}[WARN]:${COLOR_NONE} $*" >&2 -} -err() { - echo -e "${RED}[ERROR]:${COLOR_NONE} $*" >&2 -} -reenter_instructions() { - echo -e "${ORANGE}$*${COLOR_NONE}" >&2 -} - -####################################### -# Checks whether the provided filepath exists. -# Arguments: -# The filepath to check for existence. -# Optional: Whether to log an error. -####################################### -verify-filepath() { - local filepath="$1" - local no_log="$2" - if [[ ! -f "${filepath}" ]]; then - if [[ -z "${no_log}" || "${no_log}" == 'true' ]]; then - err "File ${filepath} does not exist" - fi - return 1 - fi -} - -####################################### -# Creates a commit with a message based on the specified step and file. -# Arguments: -# Which conversion step this message is for. -# The filepath of the file being converted. -####################################### -commit-step() { - local step="$1" - local filepath="$2" - if [[ -z "${step}" ]]; then - err "Missing argument (1-4)" - return 1 - fi - if [[ -z "${filepath}" ]]; then - err "Missing argument filepath" - return 1 - fi - verify-filepath "${filepath}" - if [[ $? -eq 1 ]]; then return 1; fi - - local message='' - case $1 in - 1) - message="Migrate ${filepath} to ES6 const/let" - ;; - 2) - message="Migrate ${filepath} to goog.module" - ;; - 3) - message="Migrate ${filepath} named requires" - ;; - 4) - message="clang-format ${filepath}" - ;; - *) - err 'INVALID ARGUMENT' - return 1 - ;; - esac - git add . - if [[ -z $(git status --porcelain) ]]; then - success "Nothing to commit" - return 0 - fi - git commit -m "${message}" - success "created commit with message: \"${message}\"" -} - -####################################### -# Extracts a list of properties that are accessed on the specified module name. -# Excludes any matches -# Arguments: -# The module name to find properties accessed for. -# The modules required by the specified module as a single string. -# The filepath to extract requires from. -# Optional: The top-level module. -# Outputs: -# Writes list of properties to stdout as items separated by spaces. -####################################### -getPropertiesAccessed() { - local module_name="$1" - local requires="$2" - local filepath="$3" - local top_module_name="$4" - # Get any strings that follow "$module_name.", excluding matches for - # "$module_name.prototype" and remove list item duplicates (sort -u). - local properties_accessed=$(perl -nle 'print $& while m{(?<='"${module_name}"'\.)(?!prototype)\w+}g' "${filepath}" | sort -u) - - # Get a list of any requires that are a child of $module_name. - # Ex: Blockly.utils.dom is a child of Blockly.utils, this would return "dom" - local requires_overlap=$(echo "${requires}" | perl -nle 'print $& while m{(?<='"${module_name}"'\.)\w+}g') - # Detect if there was any overlap. - if [[ -n "${requires_overlap}" ]]; then - while read -r requires_overlap_prop; do - # Removes any instances of $requires_overlap_prop. Includes regex - # lookarounds so that it does not simply match string contains. - # Ex: if $requires_overlap is "Svg", then it would update the list - # "isTargetInput mouseToSvg noEvent Svg" to - # "isTargetInput mouseToSvg noEvent " (note that mouseToSvg is unchanged). - properties_accessed=$(echo "${properties_accessed}" | perl -pe 's/(?> "${filepath}" - echo "exports = ${class_name};" >> "${filepath}" - - npm run build:deps - - success "Completed automated conversion to goog.module. Please manually review before committing." - return 0 - fi - - # No top level class. - inf 'Updating top-level property declarations...' - perl -pi -e 's/^'"${module_name}"'\.([^ ]+) =/const \1 =/g' "${filepath}" - - # Extract specific properties accessed so that properties from requires that - # are children of the module aren't changed. - # Ex: The module Blockly.utils shouldn't update Blockly.utils.dom (since it is - # a require from another module. - local requires=$(getRequires "${filepath}") - local properties_accessed=$(getPropertiesAccessed "${module_name}" "${requires}" "${filepath}") - inf "Updating local references to module..." - for property in $(echo "${properties_accessed}"); do - inf "Updating references of ${module_name}.${property} to ${property}..." - perl -pi -e 's/'"${module_name}"'\.'"${property}"'(?!\w)/'"${property}"'/g' "${filepath}" - done - - npm run build:deps - success "Completed automation for step 2. Please manually review and add exports for non-private top-level functions." -} - -####################################### -# Runs step 3 of the automated conversion. -# Arguments: -# The filepath of the file being converted. -####################################### -step3() { - inf "Extracting module name..." - local module_name=$(perl -ne 'print m/(?<=^goog\.module\('\'')([^'\'']+)/' "${filepath}") - if [[ -z "${module_name}" ]]; then - err "Could not extract module name" - return 1 - fi - inf "Extracted module name \"${module_name}\"" - - local requires=$(getRequires "${filepath}") - - # Process each require - echo "${requires}" | while read -r require; do - inf "Processing require \"${require}\"" - local usages=$(perl -nle 'print $& while m{'"${require}"'(?!'\'')}g' "${filepath}" | wc -l) - - if [[ "${usages}" -eq "0" ]]; then - warn "Unused require \"${require}\"" - continue - fi - - local require_name=$(echo "${require}" | perl -pe 's/(\w+\.)+(\w+)/\2/g') - inf "Updating require declaration for ${require}..." - perl -pi -e 's/^(goog\.(require|requireType)\('\'"${require}"\''\);)/const '"${require_name}"' = \1/' "${filepath}" - - # Parse property access of module - local direct_access_count=$(perl -nle 'print $& while m{'"${require}"'[^\.'\'']}g' "${filepath}" | wc -l) - local properties_accessed=$(getPropertiesAccessed "${require}" "${requires}" "${filepath}") - - # Remove $module_name in case it is a child of $require. - # Ex: Blockly.utils.dom would be a child of Blockly, module_overlap would be - # "utils" - local module_overlap=$(echo "${module_name}" | perl -nle 'print $& while m{(?<='"${require}"'\.)\w+}g') - if [[ -n "${module_overlap}" ]]; then - properties_accessed=$(echo "${properties_accessed}" | perl -pe 's/'"${module_overlap}"'//g') - # Trim any extra whitespace created. - properties_accessed=$(echo "${properties_accessed}" | xargs) - fi - - if [[ -n "${properties_accessed}" ]]; then - local comma_properties=$(echo "${properties_accessed}" | perl -pe 's/\s+/, /g' | perl -pe 's/, $//') - inf "Detected references of ${require}: ${comma_properties}" - - for require_prop in $(echo "${properties_accessed}"); do - inf "Updating references of ${require}.${require_prop} to ${require_name}.${require_prop}..." - perl -pi -e 's/'"${require}"'\.'"${require_prop}"'(?!\w)/'"${require_name}"'\.'"${require_prop}"'/g' "${filepath}" - done - fi - - inf "Updating direct references of ${require} to ${require_name}..." - perl -pi -e 's/'"${require}"'(?!['\''\w\.])/'"${require_name}"'/g' "${filepath}" - done - - local missing_requires=$(perl -nle'print $& while m{(? |-s ]" - echo " -h Display help and exit" - echo " -c Create a commit for the specified step [1-4]" - echo " -s Run the specified step [2-4]" -} - -####################################### -# Main entry point. -####################################### -main() { - if [ "$1" = "" ]; then - help - else - local filepath="" - # Support filepath as first argument. - verify-filepath "$1" "false" - if [[ $? -eq 0 ]]; then - filepath="$1" - shift - fi - - local command="$1" - shift - case $command in - -c) commit-step "$@" "${filepath}" ;; - -s) run-step "$@" "${filepath}" ;; - *) err "INVALID ARGUMENT ${command}";; - esac - fi -} - -main "$@" diff --git a/scripts/gulpfiles/helper_tasks.js b/scripts/gulpfiles/helper_tasks.js deleted file mode 100644 index b239d03f5fa..00000000000 --- a/scripts/gulpfiles/helper_tasks.js +++ /dev/null @@ -1,19 +0,0 @@ -/** - * @license - * Copyright 2021 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Any gulp helper functions. - */ - -// Clears the require cache to ensure the package.json is up to date. -function getPackageJson() { - delete require.cache[require.resolve('../../package.json')] - return require('../../package.json'); -} - -module.exports = { - getPackageJson: getPackageJson -} diff --git a/scripts/migration/cjs2esm b/scripts/migration/cjs2esm deleted file mode 100755 index 99a0e222322..00000000000 --- a/scripts/migration/cjs2esm +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env node - -const fs = require('fs'); -const path = require('path'); - -const filenames = process.argv.slice(2); // Trim off node and script name. - -/** Absolute path of repository root. */ -const repoPath = path.resolve(__dirname, '..', '..'); - -////////////////////////////////////////////////////////////////////// -// Process files mentioned on the command line. -////////////////////////////////////////////////////////////////////// - -/** RegExp matching require statements. */ -const requireRE = - /(?:const\s+(?:([$\w]+)|(\{[^}]*\}))\s*=\s*)?require\('([^']+)'\);/g; - -/** RegExp matching key: value pairs in destructuring assignments. */ -const keyValueRE = /([$\w]+)\s*:\s*([$\w]+)\s*(?=,|})/g; - -/** Prefix for RegExp matching a top-level declaration. */ -const declPrefix = '(?:const|let|var|(?:async\\s+)?function(?:\\s*\\*)?|class)'; - -for (const filename of filenames) { - let contents = null; - try { - contents = String(fs.readFileSync(filename)); - } catch (e) { - console.error(`error while reading ${filename}: ${e.message}`); - continue; - } - console.log(`Converting ${filename} from CJS to ESM...`); - - // Remove "use strict". - contents = contents.replace(/^\s*["']use strict["']\s*; *\n/m, ''); - - // Migrate from require to import. - contents = contents.replace( - requireRE, - function ( - orig, // Whole statement to be replaced. - name, // Name of named import of whole module (if applicable). - names, // {}-enclosed list of destructured imports. - moduleName, // Imported module name or path. - ) { - if (moduleName[0] === '.') { - // Relative path. Could check and add '.mjs' suffix if desired. - } - if (name) { - return `import * as ${name} from '${moduleName}';`; - } else if (names) { - names = names.replace(keyValueRE, '$1 as $2'); - return `import ${names} from '${moduleName}';`; - } else { - // Side-effect only require. - return `import '${moduleName}';`; - } - }, - ); - - // Find and update or remove old-style single-export assignments - // like: - // - // exports.bar = foo; // becomes export {foo as bar}; - // exports.foo = foo; // remove the export and export at declaration - // // instead, if possible. - /** @type {!Array<{name: string, re: RegExp>}>} */ - const easyExports = []; - contents = contents.replace( - /^\s*exports\.([$\w]+)\s*=\s*([$\w]+)\s*;\n/gm, - function ( - orig, // Whole statement to be replaced. - exportName, // Name to export item as. - declName, // Already-declared name for item being exported. - ) { - // Renamed exports have to be translated as-is. - if (exportName !== declName) { - return `export {${declName} as ${exportName}};\n`; - } - // OK, we're doing "export.foo = foo;". Can we update the - // declaration? We can't actualy modify it yet as we're in - // the middle of a search-and-replace on contents already, but - // we can delete the old export and later update the - // declaration into an export. - const declRE = new RegExp( - `^(\\s*)(${declPrefix}\\s+${declName})\\b`, - 'gm', - ); - if (contents.match(declRE)) { - easyExports.push({exportName, declRE}); - return ''; // Delete existing export assignment. - } else { - return `export ${exportName};\n`; // Safe fallback. - } - }, - ); - - // Find and update or remove old-style module.exports assignment - // like: - // - // module.exports = {foo, bar: baz, quux}; - // - // which becomes export {baz as bar}, with foo and quux exported at - // declaration instead, if possible. - contents = contents.replace( - /^module\.exports\s*=\s*\{([^\}]+)\};?(\n?)/m, - function ( - orig, // Whole statement to be replaced. - items, // List of items to be exported. - ) { - items = items.replace( - /( *)([$\w]+)\s*(?::\s*([$\w]+)\s*)?,?(\s*?\n?)/gm, - function ( - origItem, // Whole item being replaced. - indent, // Optional leading whitespace. - exportName, // Name to export item as. - declName, // Already-declared name being exported, if different. - newline, // Optional trailing whitespace. - ) { - if (!declName) declName = exportName; - - // Renamed exports have to be translated as-is. - if (exportName !== declName) { - return `${indent}${declName} as ${exportName},${newline}`; - } - // OK, this item has no rename. Can we update the - // declaration? We can't actualy modify it yet as we're in - // the middle of a search-and-replace on contents already, - // but we can delete the item and later update the - // declaration into an export. - const declRE = new RegExp( - `^(\\s*)(${declPrefix}\\s+${declName})\\b`, - 'gm', - ); - if (contents.match(declRE)) { - easyExports.push({exportName, declRE}); - return ''; // Delete existing item. - } else { - return `${indent}${exportName},${newline}`; // Safe fallback. - } - }, - ); - if (/^\s*$/s.test(items)) { - // No items left? - return ''; // Delete entire module.export assignment. - } else { - return `export {${items}};\n`; - } - }, - ); - - // Add 'export' to existing declarations where appropriate. - for (const {exportName, declRE} of easyExports) { - contents = contents.replace(declRE, '$1export $2'); - } - - // Write converted file with new extension. - const newFilename = filename.replace(/.c?js$/, '.mjs'); - fs.writeFileSync(newFilename, contents); - console.log(`Wrote ${newFilename}.`); -} diff --git a/scripts/migration/js2ts b/scripts/migration/js2ts deleted file mode 100755 index 068871df52b..00000000000 --- a/scripts/migration/js2ts +++ /dev/null @@ -1,168 +0,0 @@ -#!/usr/bin/env node - -const fs = require('fs'); -const path = require('path'); - -const filenames = process.argv.slice(2); // Trim off node and script name. - -////////////////////////////////////////////////////////////////////// -// Load deps files via require (since they're executalbe .js files). -////////////////////////////////////////////////////////////////////// - -/** - * Dictionary mapping goog.module ID to absolute pathname of the file - * containing the goog.declareModuleId for that ID. - * @type {!Object} - */ -const modulePaths = {}; - -/** Absolute path of repository root. */ -const repoPath = path.resolve(__dirname, '..', '..'); - -/** - * Absolute path of directory containing base.js (the version used as - * input to tsc, not the one output by it). - * @type {string} - */ -const closurePath = path.resolve(repoPath, 'closure', 'goog'); - -globalThis.goog = {}; - -/** - * Stub version of addDependency that store mappings in modulePaths. - * @param {string} relPath The path to the js file. - * @param {!Array} provides An array of strings with - * the names of the objects this file provides. - * @param {!Array} _requires An array of strings with - * the names of the objects this file requires (unused). - * @param {boolean|!Object=} opt_loadFlags Parameters indicating - * how the file must be loaded. The boolean 'true' is equivalent - * to {'module': 'goog'} for backwards-compatibility. Valid properties - * and values include {'module': 'goog'} and {'lang': 'es6'}. - */ -goog.addDependency = function (relPath, provides, _requires, opt_loadFlags) { - // Ignore any non-ESM files, as they can't be imported. - if (opt_loadFlags?.module !== 'es6') return; - - // There should be only one "provide" from an ESM, but... - for (const moduleId of provides) { - // Store absolute path to source file (i.e., treating relPath - // relative to closure/goog/, not build/src/closure/goog/). - modulePaths[moduleId] = path.resolve(closurePath, relPath); - } -}; - -// Load deps files relative to this script's location. -require(path.resolve(__dirname, '../../build/deps.js')); - -////////////////////////////////////////////////////////////////////// -// Process files mentioned on the command line. -////////////////////////////////////////////////////////////////////// - -/** RegExp matching goog.require statements. */ -const requireRE = - /(?:const\s+(?:([$\w]+)|(\{[^}]*\}))\s+=\s+)?goog.require(Type)?\('([^']+)'\);/gm; - -/** RegExp matching key: value pairs in destructuring assignments. */ -const keyValueRE = /([$\w]+)\s*:\s*([$\w]+)\s*(?=,|})/g; - -for (const filename of filenames) { - let contents = null; - try { - contents = String(fs.readFileSync(filename)); - } catch (e) { - console.error(`error while reading ${filename}: ${e.message}`); - continue; - } - console.log(`Converting ${filename} to TypeScript...`); - - // Remove "use strict". - contents = contents.replace(/^\s*["']use strict["']\s*; *\n/m, ''); - - // Migrate from goog.module to goog.declareModuleId. - const closurePathRelative = path.relative( - path.dirname(path.resolve(filename)), - closurePath, - ); - contents = contents.replace( - /^goog.module\('([$\w.]+)'\);$/m, - `import * as goog from '${closurePathRelative}/goog.js';\n` + - `goog.declareModuleId('$1');`, - ); - - // Migrate from goog.require to import. - contents = contents.replace( - requireRE, - function ( - orig, // Whole statement to be replaced. - name, // Name of named import of whole module (if applicable). - names, // {}-enclosed list of destructured imports. - type, // If truthy, it is a requireType not require. - moduleId, // goog.module ID that was goog.require()d. - ) { - const importPath = modulePaths[moduleId]; - type = type ? ' type' : ''; - if (!importPath) { - console.warn( - `Unable to migrate goog.require('${moduleId}') as no ES module path known.`, - ); - return orig; - } - let relativePath = path.relative( - path.dirname(path.resolve(filename)), - importPath, - ); - if (relativePath[0] !== '.') relativePath = './' + relativePath; - if (name) { - return `import${type} * as ${name} from '${relativePath}';`; - } else if (names) { - names = names.replace(keyValueRE, '$1 as $2'); - return `import${type} ${names} from '${relativePath}';`; - } else { - // Side-effect only require. - return `import${type} '${relativePath}';`; - } - }, - ); - - // Find and update or remove old-style export assignemnts. - /** @type {!Array<{name: string, re: RegExp>}>} */ - const easyExports = []; - contents = contents.replace( - /^\s*exports\.([$\w]+)\s*=\s*([$\w]+)\s*;\n/gm, - function ( - orig, // Whole statement to be replaced. - exportName, // Name to export item as. - declName, // Already-declared name for item being exported. - ) { - // Renamed exports have to be transalted as-is. - if (exportName !== declName) { - return `export {${declName} as ${exportName}};\n`; - } - // OK, we're doing "export.foo = foo;". Can we update the - // declaration? We can't actualy modify it yet as we're in - // the middle of a search-and-replace on contents already, but - // we can delete the old export and later update the - // declaration into an export. - const declRE = new RegExp( - `^(\\s*)((?:const|let|var|function|class)\\s+${declName})\\b`, - 'gm', - ); - if (contents.match(declRE)) { - easyExports.push({exportName, declRE}); - return ''; // Delete existing export assignment. - } else { - return `export ${exportName};\n`; // Safe fallback. - } - }, - ); - // Add 'export' to existing declarations where appropriate. - for (const {exportName, declRE} of easyExports) { - contents = contents.replace(declRE, '$1export $2'); - } - - // Write converted file with new extension. - const newFilename = filename.replace(/.js$/, '.ts'); - fs.writeFileSync(newFilename, contents); - console.log(`Wrote ${newFilename}.`); -} diff --git a/tests/browser/test/basic_playground_test.mjs b/tests/browser/test/basic_playground_test.mjs deleted file mode 100644 index c0a1f893037..00000000000 --- a/tests/browser/test/basic_playground_test.mjs +++ /dev/null @@ -1,198 +0,0 @@ -/** - * @license - * Copyright 2023 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Node.js script to run Automated tests in Chrome, via webdriver. - */ - -import * as chai from 'chai'; -import { - connect, - contextMenuSelect, - dragBlockTypeFromFlyout, - dragNthBlockFromFlyout, - PAUSE_TIME, - testFileLocations, - testSetup, -} from './test_setup.mjs'; - -async function getIsCollapsed(browser, blockId) { - return await browser.execute((blockId) => { - return Blockly.getMainWorkspace().getBlockById(blockId).isCollapsed(); - }, blockId); -} - -async function getIsDisabled(browser, blockId) { - return await browser.execute((blockId) => { - const block = Blockly.getMainWorkspace().getBlockById(blockId); - return !block.isEnabled() || block.getInheritedDisabled(); - }, blockId); -} - -async function getCommentText(browser, blockId) { - return await browser.execute((blockId) => { - return Blockly.getMainWorkspace().getBlockById(blockId).getCommentText(); - }, blockId); -} - -suite('Testing Connecting Blocks', function () { - // Setting timeout to unlimited as the webdriver takes a longer time to run than most mocha test - this.timeout(0); - - // Setup Selenium for all of the tests - suiteSetup(async function () { - this.browser = await testSetup(testFileLocations.PLAYGROUND); - }); - - test('dragging a block from the flyout results in a block on the workspace', async function () { - await dragBlockTypeFromFlyout(this.browser, 'Logic', 'controls_if', 20, 20); - const blockCount = await this.browser.execute(() => { - return Blockly.getMainWorkspace().getAllBlocks(false).length; - }); - - chai.assert.equal(blockCount, 1); - }); -}); - -/** - * These tests have to run together. Each test acts on the state left by the - * previous test, and each test has a single assertion. - */ -suite('Right Clicking on Blocks', function () { - // Setting timeout to unlimited as the webdriver takes a longer time to run than most mocha test - this.timeout(0); - - // Setup Selenium for all of the tests - suiteSetup(async function () { - this.browser = await testSetup(testFileLocations.PLAYGROUND); - this.block = await dragNthBlockFromFlyout(this.browser, 'Loops', 0, 20, 20); - }); - - test('clicking the collapse option collapses the block', async function () { - await contextMenuSelect(this.browser, this.block, 'Collapse Block'); - chai.assert.isTrue(await getIsCollapsed(this.browser, this.block.id)); - }); - - // Assumes that - test('clicking the expand option expands the block', async function () { - await contextMenuSelect(this.browser, this.block, 'Expand Block'); - chai.assert.isFalse(await getIsCollapsed(this.browser, this.block.id)); - }); - - test('clicking the disable option disables the block', async function () { - await contextMenuSelect(this.browser, this.block, 'Disable Block'); - chai.assert.isTrue(await getIsDisabled(this.browser, this.block.id)); - }); - - test('clicking the enable option enables the block', async function () { - await contextMenuSelect(this.browser, this.block, 'Enable Block'); - chai.assert.isFalse(await getIsDisabled(this.browser, this.block.id)); - }); - - test('clicking the add comment option adds a comment to the block', async function () { - await contextMenuSelect(this.browser, this.block, 'Add Comment'); - chai.assert.equal(await getCommentText(this.browser, this.block.id), ''); - }); - - test('clicking the remove comment option removes a comment from the block', async function () { - await contextMenuSelect(this.browser, this.block, 'Remove Comment'); - chai.assert.isNull(await getCommentText(this.browser, this.block.id)); - }); -}); - -suite('Disabling', function () { - // Setting timeout to unlimited as the webdriver takes a longer - // time to run than most mocha tests. - this.timeout(0); - - suiteSetup(async function () { - this.browser = await testSetup(testFileLocations.PLAYGROUND); - }); - - setup(async function () { - await this.browser.refresh(); - // Pause to allow refresh time to work. - await this.browser.pause(PAUSE_TIME + 150); - }); - - test( - 'children connected to value inputs are disabled when the ' + - 'parent is disabled', - async function () { - const parent = await dragBlockTypeFromFlyout( - this.browser, - 'Logic', - 'controls_if', - 10, - 10, - ); - const child = await dragBlockTypeFromFlyout( - this.browser, - 'Logic', - 'logic_boolean', - 110, - 110, - ); - await connect(this.browser, child, 'OUTPUT', parent, 'IF0'); - await this.browser.pause(PAUSE_TIME); - await contextMenuSelect(this.browser, parent, 'Disable Block'); - - chai.assert.isTrue(await getIsDisabled(this.browser, child.id)); - }, - ); - - test( - 'children connected to statement inputs are disabled when the ' + - 'parent is disabled', - async function () { - const parent = await dragBlockTypeFromFlyout( - this.browser, - 'Logic', - 'controls_if', - 10, - 10, - ); - const child = await dragBlockTypeFromFlyout( - this.browser, - 'Logic', - 'controls_if', - 110, - 110, - ); - await connect(this.browser, child, 'PREVIOUS', parent, 'DO0'); - - await contextMenuSelect(this.browser, parent, 'Disable Block'); - - chai.assert.isTrue(await getIsDisabled(this.browser, child.id)); - }, - ); - - test( - 'children connected to next connections are not disabled when the ' + - 'parent is disabled', - async function () { - const parent = await dragBlockTypeFromFlyout( - this.browser, - 'Logic', - 'controls_if', - 10, - 10, - ); - const child = await dragBlockTypeFromFlyout( - this.browser, - 'Logic', - 'controls_if', - 110, - 110, - ); - await connect(this.browser, child, 'PREVIOUS', parent, 'NEXT'); - - await contextMenuSelect(this.browser, parent, 'Disable Block'); - - chai.assert.isFalse(await getIsDisabled(this.browser, child.id)); - }, - ); -}); diff --git a/tests/mocha/astnode_test.js b/tests/mocha/astnode_test.js deleted file mode 100644 index 7ffe8efb90a..00000000000 --- a/tests/mocha/astnode_test.js +++ /dev/null @@ -1,850 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import {ASTNode} from '../../build/src/core/keyboard_nav/ast_node.js'; -import {assert} from '../../node_modules/chai/chai.js'; -import { - sharedTestSetup, - sharedTestTeardown, - workspaceTeardown, -} from './test_helpers/setup_teardown.js'; - -suite('ASTNode', function () { - setup(function () { - sharedTestSetup.call(this); - Blockly.defineBlocksWithJsonArray([ - { - 'type': 'input_statement', - 'message0': '%1 %2 %3 %4', - 'args0': [ - { - 'type': 'field_input', - 'name': 'NAME', - 'text': 'default', - }, - { - 'type': 'field_input', - 'name': 'NAME', - 'text': 'default', - }, - { - 'type': 'input_value', - 'name': 'NAME', - }, - { - 'type': 'input_statement', - 'name': 'NAME', - }, - ], - 'previousStatement': null, - 'nextStatement': null, - 'colour': 230, - 'tooltip': '', - 'helpUrl': '', - }, - { - 'type': 'value_input', - 'message0': '%1', - 'args0': [ - { - 'type': 'input_value', - 'name': 'NAME', - }, - ], - 'colour': 230, - 'tooltip': '', - 'helpUrl': '', - }, - { - 'type': 'field_input', - 'message0': '%1', - 'args0': [ - { - 'type': 'field_input', - 'name': 'NAME', - 'text': 'default', - }, - ], - 'output': null, - 'colour': 230, - 'tooltip': '', - 'helpUrl': '', - }, - ]); - this.workspace = new Blockly.Workspace(); - this.cursor = this.workspace.cursor; - const statementInput1 = this.workspace.newBlock('input_statement'); - const statementInput2 = this.workspace.newBlock('input_statement'); - const statementInput3 = this.workspace.newBlock('input_statement'); - const statementInput4 = this.workspace.newBlock('input_statement'); - const fieldWithOutput = this.workspace.newBlock('field_input'); - const valueInput = this.workspace.newBlock('value_input'); - - statementInput1.nextConnection.connect(statementInput2.previousConnection); - statementInput1.inputList[0].connection.connect( - fieldWithOutput.outputConnection, - ); - statementInput2.inputList[1].connection.connect( - statementInput3.previousConnection, - ); - - this.blocks = { - statementInput1: statementInput1, - statementInput2: statementInput2, - statementInput3: statementInput3, - statementInput4: statementInput4, - fieldWithOutput: fieldWithOutput, - valueInput: valueInput, - }; - }); - teardown(function () { - sharedTestTeardown.call(this); - }); - - suite('HelperFunctions', function () { - test('findNextForInput', function () { - const input = this.blocks.statementInput1.inputList[0]; - const input2 = this.blocks.statementInput1.inputList[1]; - const connection = input.connection; - const node = ASTNode.createConnectionNode(connection); - const newASTNode = node.findNextForInput(input); - assert.equal(newASTNode.getLocation(), input2.connection); - }); - - test('findPrevForInput', function () { - const input = this.blocks.statementInput1.inputList[0]; - const input2 = this.blocks.statementInput1.inputList[1]; - const connection = input2.connection; - const node = ASTNode.createConnectionNode(connection); - const newASTNode = node.findPrevForInput(input2); - assert.equal(newASTNode.getLocation(), input.connection); - }); - - test('findNextForField', function () { - const field = this.blocks.statementInput1.inputList[0].fieldRow[0]; - const field2 = this.blocks.statementInput1.inputList[0].fieldRow[1]; - const node = ASTNode.createFieldNode(field); - const newASTNode = node.findNextForField(field); - assert.equal(newASTNode.getLocation(), field2); - }); - - test('findPrevForField', function () { - const field = this.blocks.statementInput1.inputList[0].fieldRow[0]; - const field2 = this.blocks.statementInput1.inputList[0].fieldRow[1]; - const node = ASTNode.createFieldNode(field2); - const newASTNode = node.findPrevForField(field2); - assert.equal(newASTNode.getLocation(), field); - }); - - test('navigateBetweenStacks_Forward', function () { - const node = new ASTNode( - ASTNode.types.NEXT, - this.blocks.statementInput1.nextConnection, - ); - const newASTNode = node.navigateBetweenStacks(true); - assert.equal(newASTNode.getLocation(), this.blocks.statementInput4); - }); - - test('navigateBetweenStacks_Backward', function () { - const node = new ASTNode( - ASTNode.types.BLOCK, - this.blocks.statementInput4, - ); - const newASTNode = node.navigateBetweenStacks(false); - assert.equal(newASTNode.getLocation(), this.blocks.statementInput1); - }); - test('getOutAstNodeForBlock', function () { - const node = new ASTNode( - ASTNode.types.BLOCK, - this.blocks.statementInput2, - ); - const newASTNode = node.getOutAstNodeForBlock( - this.blocks.statementInput2, - ); - assert.equal(newASTNode.getLocation(), this.blocks.statementInput1); - }); - test('getOutAstNodeForBlock_OneBlock', function () { - const node = new ASTNode( - ASTNode.types.BLOCK, - this.blocks.statementInput4, - ); - const newASTNode = node.getOutAstNodeForBlock( - this.blocks.statementInput4, - ); - assert.equal(newASTNode.getLocation(), this.blocks.statementInput4); - }); - test('findFirstFieldOrInput_', function () { - const node = new ASTNode( - ASTNode.types.BLOCK, - this.blocks.statementInput4, - ); - const field = this.blocks.statementInput4.inputList[0].fieldRow[0]; - const newASTNode = node.findFirstFieldOrInput( - this.blocks.statementInput4, - ); - assert.equal(newASTNode.getLocation(), field); - }); - }); - - suite('NavigationFunctions', function () { - setup(function () { - Blockly.defineBlocksWithJsonArray([ - { - 'type': 'top_connection', - 'message0': '', - 'previousStatement': null, - 'colour': 230, - 'tooltip': '', - 'helpUrl': '', - }, - { - 'type': 'start_block', - 'message0': '', - 'nextStatement': null, - 'colour': 230, - 'tooltip': '', - 'helpUrl': '', - }, - { - 'type': 'fields_and_input', - 'message0': '%1 hi %2 %3 %4', - 'args0': [ - { - 'type': 'field_input', - 'name': 'NAME', - 'text': 'default', - }, - { - 'type': 'input_dummy', - }, - { - 'type': 'field_input', - 'name': 'NAME', - 'text': 'default', - }, - { - 'type': 'input_value', - 'name': 'NAME', - }, - ], - 'previousStatement': null, - 'nextStatement': null, - 'colour': 230, - 'tooltip': '', - 'helpUrl': '', - }, - { - 'type': 'two_fields', - 'message0': '%1 hi', - 'args0': [ - { - 'type': 'field_input', - 'name': 'NAME', - 'text': 'default', - }, - ], - 'colour': 230, - 'tooltip': '', - 'helpUrl': '', - }, - { - 'type': 'fields_and_input2', - 'message0': '%1 %2 %3 hi %4 bye', - 'args0': [ - { - 'type': 'input_value', - 'name': 'NAME', - }, - { - 'type': 'field_input', - 'name': 'NAME', - 'text': 'default', - }, - { - 'type': 'input_value', - 'name': 'NAME', - }, - { - 'type': 'input_statement', - 'name': 'NAME', - }, - ], - 'colour': 230, - 'tooltip': '', - 'helpUrl': '', - }, - { - 'type': 'dummy_input', - 'message0': 'Hello', - 'colour': 230, - 'tooltip': '', - 'helpUrl': '', - }, - { - 'type': 'dummy_inputValue', - 'message0': 'Hello %1 %2', - 'args0': [ - { - 'type': 'input_dummy', - }, - { - 'type': 'input_value', - 'name': 'NAME', - }, - ], - 'colour': 230, - 'tooltip': '', - 'helpUrl': '', - }, - { - 'type': 'output_next', - 'message0': '', - 'output': null, - 'colour': 230, - 'tooltip': '', - 'helpUrl': '', - 'nextStatement': null, - }, - ]); - const noNextConnection = this.workspace.newBlock('top_connection'); - const fieldAndInputs = this.workspace.newBlock('fields_and_input'); - const twoFields = this.workspace.newBlock('two_fields'); - const fieldAndInputs2 = this.workspace.newBlock('fields_and_input2'); - const noPrevConnection = this.workspace.newBlock('start_block'); - this.blocks.noNextConnection = noNextConnection; - this.blocks.fieldAndInputs = fieldAndInputs; - this.blocks.twoFields = twoFields; - this.blocks.fieldAndInputs2 = fieldAndInputs2; - this.blocks.noPrevConnection = noPrevConnection; - - const dummyInput = this.workspace.newBlock('dummy_input'); - const dummyInputValue = this.workspace.newBlock('dummy_inputValue'); - const fieldWithOutput2 = this.workspace.newBlock('field_input'); - this.blocks.dummyInput = dummyInput; - this.blocks.dummyInputValue = dummyInputValue; - this.blocks.fieldWithOutput2 = fieldWithOutput2; - - const secondBlock = this.workspace.newBlock('input_statement'); - const outputNextBlock = this.workspace.newBlock('output_next'); - this.blocks.secondBlock = secondBlock; - this.blocks.outputNextBlock = outputNextBlock; - }); - suite('Next', function () { - setup(function () { - this.singleBlockWorkspace = new Blockly.Workspace(); - const singleBlock = this.singleBlockWorkspace.newBlock('two_fields'); - this.blocks.singleBlock = singleBlock; - }); - teardown(function () { - workspaceTeardown.call(this, this.singleBlockWorkspace); - }); - - test('fromPreviousToBlock', function () { - const prevConnection = this.blocks.statementInput1.previousConnection; - const node = ASTNode.createConnectionNode(prevConnection); - const nextNode = node.next(); - assert.equal(nextNode.getLocation(), this.blocks.statementInput1); - }); - test('fromBlockToNext', function () { - const nextConnection = this.blocks.statementInput1.nextConnection; - const node = ASTNode.createBlockNode(this.blocks.statementInput1); - const nextNode = node.next(); - assert.equal(nextNode.getLocation(), nextConnection); - }); - test('fromBlockToNull', function () { - const node = ASTNode.createBlockNode(this.blocks.noNextConnection); - const nextNode = node.next(); - assert.isNull(nextNode); - }); - test('fromNextToPrevious', function () { - const nextConnection = this.blocks.statementInput1.nextConnection; - const prevConnection = this.blocks.statementInput2.previousConnection; - const node = ASTNode.createConnectionNode(nextConnection); - const nextNode = node.next(); - assert.equal(nextNode.getLocation(), prevConnection); - }); - test('fromNextToNull', function () { - const nextConnection = this.blocks.statementInput2.nextConnection; - const node = ASTNode.createConnectionNode(nextConnection); - const nextNode = node.next(); - assert.isNull(nextNode); - }); - test('fromInputToInput', function () { - const input = this.blocks.statementInput1.inputList[0]; - const inputConnection = - this.blocks.statementInput1.inputList[1].connection; - const node = ASTNode.createInputNode(input); - const nextNode = node.next(); - assert.equal(nextNode.getLocation(), inputConnection); - }); - test('fromInputToStatementInput', function () { - const input = this.blocks.fieldAndInputs2.inputList[1]; - const inputConnection = - this.blocks.fieldAndInputs2.inputList[2].connection; - const node = ASTNode.createInputNode(input); - const nextNode = node.next(); - assert.equal(nextNode.getLocation(), inputConnection); - }); - test('fromInputToField', function () { - const input = this.blocks.fieldAndInputs2.inputList[0]; - const field = this.blocks.fieldAndInputs2.inputList[1].fieldRow[0]; - const node = ASTNode.createInputNode(input); - const nextNode = node.next(); - assert.equal(nextNode.getLocation(), field); - }); - test('fromInputToNull', function () { - const input = this.blocks.fieldAndInputs2.inputList[2]; - const node = ASTNode.createInputNode(input); - const nextNode = node.next(); - assert.isNull(nextNode); - }); - test('fromOutputToBlock', function () { - const output = this.blocks.fieldWithOutput.outputConnection; - const node = ASTNode.createConnectionNode(output); - const nextNode = node.next(); - assert.equal(nextNode.getLocation(), this.blocks.fieldWithOutput); - }); - test('fromFieldToInput', function () { - const field = this.blocks.statementInput1.inputList[0].fieldRow[1]; - const inputConnection = - this.blocks.statementInput1.inputList[0].connection; - const node = ASTNode.createFieldNode(field); - const nextNode = node.next(); - assert.equal(nextNode.getLocation(), inputConnection); - }); - test('fromFieldToField', function () { - const field = this.blocks.fieldAndInputs.inputList[0].fieldRow[0]; - const node = ASTNode.createFieldNode(field); - const field2 = this.blocks.fieldAndInputs.inputList[1].fieldRow[0]; - const nextNode = node.next(); - assert.equal(nextNode.getLocation(), field2); - }); - test('fromFieldToNull', function () { - const field = this.blocks.twoFields.inputList[0].fieldRow[0]; - const node = ASTNode.createFieldNode(field); - const nextNode = node.next(); - assert.isNull(nextNode); - }); - test('fromStackToStack', function () { - const node = ASTNode.createStackNode(this.blocks.statementInput1); - const nextNode = node.next(); - assert.equal(nextNode.getLocation(), this.blocks.statementInput4); - assert.equal(nextNode.getType(), ASTNode.types.STACK); - }); - test('fromStackToNull', function () { - const node = ASTNode.createStackNode(this.blocks.singleBlock); - const nextNode = node.next(); - assert.isNull(nextNode); - }); - }); - - suite('Previous', function () { - test('fromPreviousToNull', function () { - const prevConnection = this.blocks.statementInput1.previousConnection; - const node = ASTNode.createConnectionNode(prevConnection); - const prevNode = node.prev(); - assert.isNull(prevNode); - }); - test('fromPreviousToNext', function () { - const prevConnection = this.blocks.statementInput2.previousConnection; - const node = ASTNode.createConnectionNode(prevConnection); - const prevNode = node.prev(); - const nextConnection = this.blocks.statementInput1.nextConnection; - assert.equal(prevNode.getLocation(), nextConnection); - }); - test('fromPreviousToInput', function () { - const prevConnection = this.blocks.statementInput3.previousConnection; - const node = ASTNode.createConnectionNode(prevConnection); - const prevNode = node.prev(); - assert.isNull(prevNode); - }); - test('fromBlockToPrevious', function () { - const node = ASTNode.createBlockNode(this.blocks.statementInput1); - const prevNode = node.prev(); - const prevConnection = this.blocks.statementInput1.previousConnection; - assert.equal(prevNode.getLocation(), prevConnection); - }); - test('fromBlockToNull', function () { - const node = ASTNode.createBlockNode(this.blocks.noPrevConnection); - const prevNode = node.prev(); - assert.isNull(prevNode); - }); - test('fromBlockToOutput', function () { - const node = ASTNode.createBlockNode(this.blocks.fieldWithOutput); - const prevNode = node.prev(); - const outputConnection = this.blocks.fieldWithOutput.outputConnection; - assert.equal(prevNode.getLocation(), outputConnection); - }); - test('fromNextToBlock', function () { - const nextConnection = this.blocks.statementInput1.nextConnection; - const node = ASTNode.createConnectionNode(nextConnection); - const prevNode = node.prev(); - assert.equal(prevNode.getLocation(), this.blocks.statementInput1); - }); - test('fromInputToField', function () { - const input = this.blocks.statementInput1.inputList[0]; - const node = ASTNode.createInputNode(input); - const prevNode = node.prev(); - assert.equal(prevNode.getLocation(), input.fieldRow[1]); - }); - test('fromInputToNull', function () { - const input = this.blocks.fieldAndInputs2.inputList[0]; - const node = ASTNode.createInputNode(input); - const prevNode = node.prev(); - assert.isNull(prevNode); - }); - test('fromInputToInput', function () { - const input = this.blocks.fieldAndInputs2.inputList[2]; - const inputConnection = - this.blocks.fieldAndInputs2.inputList[1].connection; - const node = ASTNode.createInputNode(input); - const prevNode = node.prev(); - assert.equal(prevNode.getLocation(), inputConnection); - }); - test('fromOutputToNull', function () { - const output = this.blocks.fieldWithOutput.outputConnection; - const node = ASTNode.createConnectionNode(output); - const prevNode = node.prev(); - assert.isNull(prevNode); - }); - test('fromFieldToNull', function () { - const field = this.blocks.statementInput1.inputList[0].fieldRow[0]; - const node = ASTNode.createFieldNode(field); - const prevNode = node.prev(); - assert.isNull(prevNode); - }); - test('fromFieldToInput', function () { - const field = this.blocks.fieldAndInputs2.inputList[1].fieldRow[0]; - const inputConnection = - this.blocks.fieldAndInputs2.inputList[0].connection; - const node = ASTNode.createFieldNode(field); - const prevNode = node.prev(); - assert.equal(prevNode.getLocation(), inputConnection); - }); - test('fromFieldToField', function () { - const field = this.blocks.fieldAndInputs.inputList[1].fieldRow[0]; - const field2 = this.blocks.fieldAndInputs.inputList[0].fieldRow[0]; - const node = ASTNode.createFieldNode(field); - const prevNode = node.prev(); - assert.equal(prevNode.getLocation(), field2); - }); - test('fromStackToStack', function () { - const node = ASTNode.createStackNode(this.blocks.statementInput4); - const prevNode = node.prev(); - assert.equal(prevNode.getLocation(), this.blocks.statementInput1); - assert.equal(prevNode.getType(), ASTNode.types.STACK); - }); - }); - - suite('In', function () { - setup(function () { - this.emptyWorkspace = new Blockly.Workspace(); - }); - teardown(function () { - workspaceTeardown.call(this, this.emptyWorkspace); - }); - - test('fromInputToOutput', function () { - const input = this.blocks.statementInput1.inputList[0]; - const node = ASTNode.createInputNode(input); - const inNode = node.in(); - const outputConnection = this.blocks.fieldWithOutput.outputConnection; - assert.equal(inNode.getLocation(), outputConnection); - }); - test('fromInputToNull', function () { - const input = this.blocks.statementInput2.inputList[0]; - const node = ASTNode.createInputNode(input); - const inNode = node.in(); - assert.isNull(inNode); - }); - test('fromInputToPrevious', function () { - const input = this.blocks.statementInput2.inputList[1]; - const previousConnection = - this.blocks.statementInput3.previousConnection; - const node = ASTNode.createInputNode(input); - const inNode = node.in(); - assert.equal(inNode.getLocation(), previousConnection); - }); - test('fromBlockToInput', function () { - const input = this.blocks.valueInput.inputList[0]; - const node = ASTNode.createBlockNode(this.blocks.valueInput); - const inNode = node.in(); - assert.equal(inNode.getLocation(), input.connection); - }); - test('fromBlockToField', function () { - const node = ASTNode.createBlockNode(this.blocks.statementInput1); - const inNode = node.in(); - const field = this.blocks.statementInput1.inputList[0].fieldRow[0]; - assert.equal(inNode.getLocation(), field); - }); - test('fromBlockToPrevious', function () { - const prevConnection = this.blocks.statementInput4.previousConnection; - const node = ASTNode.createStackNode(this.blocks.statementInput4); - const inNode = node.in(); - assert.equal(inNode.getLocation(), prevConnection); - assert.equal(inNode.getType(), ASTNode.types.PREVIOUS); - }); - test('fromBlockToNull_DummyInput', function () { - const node = ASTNode.createBlockNode(this.blocks.dummyInput); - const inNode = node.in(); - assert.isNull(inNode); - }); - test('fromBlockToInput_DummyInputValue', function () { - const node = ASTNode.createBlockNode(this.blocks.dummyInputValue); - const inputConnection = - this.blocks.dummyInputValue.inputList[1].connection; - const inNode = node.in(); - assert.equal(inNode.getLocation(), inputConnection); - }); - test('fromOuputToNull', function () { - const output = this.blocks.fieldWithOutput.outputConnection; - const node = ASTNode.createConnectionNode(output); - const inNode = node.in(); - assert.isNull(inNode); - }); - test('fromFieldToNull', function () { - const field = this.blocks.statementInput1.inputList[0].fieldRow[0]; - const node = ASTNode.createFieldNode(field); - const inNode = node.in(); - assert.isNull(inNode); - }); - test('fromWorkspaceToStack', function () { - const coordinate = new Blockly.utils.Coordinate(100, 100); - const node = ASTNode.createWorkspaceNode(this.workspace, coordinate); - const inNode = node.in(); - assert.equal(inNode.getLocation(), this.workspace.getTopBlocks()[0]); - assert.equal(inNode.getType(), ASTNode.types.STACK); - }); - test('fromWorkspaceToNull', function () { - const coordinate = new Blockly.utils.Coordinate(100, 100); - const node = ASTNode.createWorkspaceNode( - this.emptyWorkspace, - coordinate, - ); - const inNode = node.in(); - assert.isNull(inNode); - }); - test('fromStackToPrevious', function () { - const node = ASTNode.createStackNode(this.blocks.statementInput1); - const previous = this.blocks.statementInput1.previousConnection; - const inNode = node.in(); - assert.equal(inNode.getLocation(), previous); - assert.equal(inNode.getType(), ASTNode.types.PREVIOUS); - }); - test('fromStackToOutput', function () { - const node = ASTNode.createStackNode(this.blocks.fieldWithOutput2); - const output = this.blocks.fieldWithOutput2.outputConnection; - const inNode = node.in(); - assert.equal(inNode.getLocation(), output); - assert.equal(inNode.getType(), ASTNode.types.OUTPUT); - }); - test('fromStackToBlock', function () { - const node = ASTNode.createStackNode(this.blocks.dummyInput); - const inNode = node.in(); - assert.equal(inNode.getLocation(), this.blocks.dummyInput); - assert.equal(inNode.getType(), ASTNode.types.BLOCK); - }); - }); - - suite('Out', function () { - setup(function () { - const secondBlock = this.blocks.secondBlock; - const outputNextBlock = this.blocks.outputNextBlock; - this.blocks.noPrevConnection.nextConnection.connect( - secondBlock.previousConnection, - ); - secondBlock.inputList[0].connection.connect( - outputNextBlock.outputConnection, - ); - }); - - test('fromInputToBlock', function () { - const input = this.blocks.statementInput1.inputList[0]; - const node = ASTNode.createInputNode(input); - const outNode = node.out(); - assert.equal(outNode.getType(), ASTNode.types.BLOCK); - assert.equal(outNode.getLocation(), this.blocks.statementInput1); - }); - test('fromOutputToInput', function () { - const output = this.blocks.fieldWithOutput.outputConnection; - const node = ASTNode.createConnectionNode(output); - const outNode = node.out(); - assert.equal(outNode.getType(), ASTNode.types.INPUT); - assert.equal( - outNode.getLocation(), - this.blocks.statementInput1.inputList[0].connection, - ); - }); - test('fromOutputToStack', function () { - const output = this.blocks.fieldWithOutput2.outputConnection; - const node = ASTNode.createConnectionNode(output); - const outNode = node.out(); - assert.equal(outNode.getType(), ASTNode.types.STACK); - assert.equal(outNode.getLocation(), this.blocks.fieldWithOutput2); - }); - test('fromFieldToBlock', function () { - const field = this.blocks.statementInput1.inputList[0].fieldRow[0]; - const node = ASTNode.createFieldNode(field); - const outNode = node.out(); - assert.equal(outNode.getType(), ASTNode.types.BLOCK); - assert.equal(outNode.getLocation(), this.blocks.statementInput1); - }); - test('fromStackToWorkspace', function () { - const stub = sinon - .stub(this.blocks.statementInput4, 'getRelativeToSurfaceXY') - .returns({x: 10, y: 10}); - const node = ASTNode.createStackNode(this.blocks.statementInput4); - const outNode = node.out(); - assert.equal(outNode.getType(), ASTNode.types.WORKSPACE); - assert.equal(outNode.wsCoordinate.x, 10); - assert.equal(outNode.wsCoordinate.y, -10); - stub.restore(); - }); - test('fromPreviousToInput', function () { - const previous = this.blocks.statementInput3.previousConnection; - const inputConnection = - this.blocks.statementInput2.inputList[1].connection; - const node = ASTNode.createConnectionNode(previous); - const outNode = node.out(); - assert.equal(outNode.getType(), ASTNode.types.INPUT); - assert.equal(outNode.getLocation(), inputConnection); - }); - test('fromPreviousToStack', function () { - const previous = this.blocks.statementInput2.previousConnection; - const node = ASTNode.createConnectionNode(previous); - const outNode = node.out(); - assert.equal(outNode.getType(), ASTNode.types.STACK); - assert.equal(outNode.getLocation(), this.blocks.statementInput1); - }); - test('fromNextToInput', function () { - const next = this.blocks.statementInput3.nextConnection; - const inputConnection = - this.blocks.statementInput2.inputList[1].connection; - const node = ASTNode.createConnectionNode(next); - const outNode = node.out(); - assert.equal(outNode.getType(), ASTNode.types.INPUT); - assert.equal(outNode.getLocation(), inputConnection); - }); - test('fromNextToStack', function () { - const next = this.blocks.statementInput2.nextConnection; - const node = ASTNode.createConnectionNode(next); - const outNode = node.out(); - assert.equal(outNode.getType(), ASTNode.types.STACK); - assert.equal(outNode.getLocation(), this.blocks.statementInput1); - }); - test('fromNextToStack_NoPreviousConnection', function () { - const next = this.blocks.secondBlock.nextConnection; - const node = ASTNode.createConnectionNode(next); - const outNode = node.out(); - assert.equal(outNode.getType(), ASTNode.types.STACK); - assert.equal(outNode.getLocation(), this.blocks.noPrevConnection); - }); - /** - * This is where there is a block with both an output connection and a - * next connection attached to an input. - */ - test('fromNextToInput_OutputAndPreviousConnection', function () { - const next = this.blocks.outputNextBlock.nextConnection; - const node = ASTNode.createConnectionNode(next); - const outNode = node.out(); - assert.equal(outNode.getType(), ASTNode.types.INPUT); - assert.equal( - outNode.getLocation(), - this.blocks.secondBlock.inputList[0].connection, - ); - }); - test('fromBlockToStack', function () { - const node = ASTNode.createBlockNode(this.blocks.statementInput2); - const outNode = node.out(); - assert.equal(outNode.getType(), ASTNode.types.STACK); - assert.equal(outNode.getLocation(), this.blocks.statementInput1); - }); - test('fromBlockToInput', function () { - const input = this.blocks.statementInput2.inputList[1].connection; - const node = ASTNode.createBlockNode(this.blocks.statementInput3); - const outNode = node.out(); - assert.equal(outNode.getType(), ASTNode.types.INPUT); - assert.equal(outNode.getLocation(), input); - }); - test('fromTopBlockToStack', function () { - const node = ASTNode.createBlockNode(this.blocks.statementInput1); - const outNode = node.out(); - assert.equal(outNode.getType(), ASTNode.types.STACK); - assert.equal(outNode.getLocation(), this.blocks.statementInput1); - }); - test('fromBlockToStack_OutputConnection', function () { - const node = ASTNode.createBlockNode(this.blocks.fieldWithOutput2); - const outNode = node.out(); - assert.equal(outNode.getType(), ASTNode.types.STACK); - assert.equal(outNode.getLocation(), this.blocks.fieldWithOutput2); - }); - test('fromBlockToInput_OutputConnection', function () { - const node = ASTNode.createBlockNode(this.blocks.outputNextBlock); - const inputConnection = this.blocks.secondBlock.inputList[0].connection; - const outNode = node.out(); - assert.equal(outNode.getType(), ASTNode.types.INPUT); - assert.equal(outNode.getLocation(), inputConnection); - }); - }); - - suite('createFunctions', function () { - test('createFieldNode', function () { - const field = this.blocks.statementInput1.inputList[0].fieldRow[0]; - const node = ASTNode.createFieldNode(field); - assert.equal(node.getLocation(), field); - assert.equal(node.getType(), ASTNode.types.FIELD); - assert.isFalse(node.isConnection()); - }); - test('createConnectionNode', function () { - const prevConnection = this.blocks.statementInput4.previousConnection; - const node = ASTNode.createConnectionNode(prevConnection); - assert.equal(node.getLocation(), prevConnection); - assert.equal(node.getType(), ASTNode.types.PREVIOUS); - assert.isTrue(node.isConnection()); - }); - test('createInputNode', function () { - const input = this.blocks.statementInput1.inputList[0]; - const node = ASTNode.createInputNode(input); - assert.equal(node.getLocation(), input.connection); - assert.equal(node.getType(), ASTNode.types.INPUT); - assert.isTrue(node.isConnection()); - }); - test('createWorkspaceNode', function () { - const coordinate = new Blockly.utils.Coordinate(100, 100); - const node = ASTNode.createWorkspaceNode(this.workspace, coordinate); - assert.equal(node.getLocation(), this.workspace); - assert.equal(node.getType(), ASTNode.types.WORKSPACE); - assert.equal(node.getWsCoordinate(), coordinate); - assert.isFalse(node.isConnection()); - }); - test('createStatementConnectionNode', function () { - const nextConnection = - this.blocks.statementInput1.inputList[1].connection; - const inputConnection = - this.blocks.statementInput1.inputList[1].connection; - const node = ASTNode.createConnectionNode(nextConnection); - assert.equal(node.getLocation(), inputConnection); - assert.equal(node.getType(), ASTNode.types.INPUT); - assert.isTrue(node.isConnection()); - }); - test('createTopNode-previous', function () { - const block = this.blocks.statementInput1; - const topNode = ASTNode.createTopNode(block); - assert.equal(topNode.getLocation(), block.previousConnection); - }); - test('createTopNode-block', function () { - const block = this.blocks.noPrevConnection; - const topNode = ASTNode.createTopNode(block); - assert.equal(topNode.getLocation(), block); - }); - test('createTopNode-output', function () { - const block = this.blocks.outputNextBlock; - const topNode = ASTNode.createTopNode(block); - assert.equal(topNode.getLocation(), block.outputConnection); - }); - }); - }); -}); diff --git a/tests/mocha/cursor_test.js b/tests/mocha/cursor_test.js deleted file mode 100644 index bb5026d7ac3..00000000000 --- a/tests/mocha/cursor_test.js +++ /dev/null @@ -1,134 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import {ASTNode} from '../../build/src/core/keyboard_nav/ast_node.js'; -import {assert} from '../../node_modules/chai/chai.js'; -import { - sharedTestSetup, - sharedTestTeardown, -} from './test_helpers/setup_teardown.js'; - -suite('Cursor', function () { - setup(function () { - sharedTestSetup.call(this); - Blockly.defineBlocksWithJsonArray([ - { - 'type': 'input_statement', - 'message0': '%1 %2 %3 %4', - 'args0': [ - { - 'type': 'field_input', - 'name': 'NAME', - 'text': 'default', - }, - { - 'type': 'field_input', - 'name': 'NAME', - 'text': 'default', - }, - { - 'type': 'input_value', - 'name': 'NAME', - }, - { - 'type': 'input_statement', - 'name': 'NAME', - }, - ], - 'previousStatement': null, - 'nextStatement': null, - 'colour': 230, - 'tooltip': '', - 'helpUrl': '', - }, - { - 'type': 'field_input', - 'message0': '%1', - 'args0': [ - { - 'type': 'field_input', - 'name': 'NAME', - 'text': 'default', - }, - ], - 'output': null, - 'colour': 230, - 'tooltip': '', - 'helpUrl': '', - }, - ]); - this.workspace = Blockly.inject('blocklyDiv', {}); - this.cursor = this.workspace.getCursor(); - const blockA = this.workspace.newBlock('input_statement'); - const blockB = this.workspace.newBlock('input_statement'); - const blockC = this.workspace.newBlock('input_statement'); - const blockD = this.workspace.newBlock('input_statement'); - const blockE = this.workspace.newBlock('field_input'); - - blockA.nextConnection.connect(blockB.previousConnection); - blockA.inputList[0].connection.connect(blockE.outputConnection); - blockB.inputList[1].connection.connect(blockC.previousConnection); - this.cursor.drawer = null; - this.blocks = { - A: blockA, - B: blockB, - C: blockC, - D: blockD, - E: blockE, - }; - }); - teardown(function () { - sharedTestTeardown.call(this); - }); - - test('Next - From a Previous skip over next connection and block', function () { - const prevNode = ASTNode.createConnectionNode( - this.blocks.A.previousConnection, - ); - this.cursor.setCurNode(prevNode); - this.cursor.next(); - const curNode = this.cursor.getCurNode(); - assert.equal(curNode.getLocation(), this.blocks.B.previousConnection); - }); - test('Next - From last block in a stack go to next connection', function () { - const prevNode = ASTNode.createConnectionNode( - this.blocks.B.previousConnection, - ); - this.cursor.setCurNode(prevNode); - this.cursor.next(); - const curNode = this.cursor.getCurNode(); - assert.equal(curNode.getLocation(), this.blocks.B.nextConnection); - }); - - test('In - From output connection', function () { - const fieldBlock = this.blocks.E; - const outputNode = ASTNode.createConnectionNode( - fieldBlock.outputConnection, - ); - this.cursor.setCurNode(outputNode); - this.cursor.in(); - const curNode = this.cursor.getCurNode(); - assert.equal(curNode.getLocation(), fieldBlock.inputList[0].fieldRow[0]); - }); - - test('Prev - From previous connection skip over next connection', function () { - const prevConnection = this.blocks.B.previousConnection; - const prevConnectionNode = ASTNode.createConnectionNode(prevConnection); - this.cursor.setCurNode(prevConnectionNode); - this.cursor.prev(); - const curNode = this.cursor.getCurNode(); - assert.equal(curNode.getLocation(), this.blocks.A.previousConnection); - }); - - test('Out - From field skip over block node', function () { - const field = this.blocks.E.inputList[0].fieldRow[0]; - const fieldNode = ASTNode.createFieldNode(field); - this.cursor.setCurNode(fieldNode); - this.cursor.out(); - const curNode = this.cursor.getCurNode(); - assert.equal(curNode.getLocation(), this.blocks.E.outputConnection); - }); -}); diff --git a/tests/mocha/dropdowndiv_test.js b/tests/mocha/dropdowndiv_test.js deleted file mode 100644 index 32109bfcadd..00000000000 --- a/tests/mocha/dropdowndiv_test.js +++ /dev/null @@ -1,116 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import {assert} from '../../node_modules/chai/chai.js'; -import { - sharedTestSetup, - sharedTestTeardown, -} from './test_helpers/setup_teardown.js'; - -suite('DropDownDiv', function () { - suite('Positioning', function () { - setup(function () { - sharedTestSetup.call(this); - this.boundsStub = sinon - .stub(Blockly.DropDownDiv.TEST_ONLY, 'getBoundsInfo') - .returns({ - left: 0, - right: 100, - top: 0, - bottom: 100, - width: 100, - height: 100, - }); - this.sizeStub = sinon - .stub(Blockly.utils.style.TEST_ONLY, 'getSizeInternal') - .returns({ - width: 60, - height: 60, - }); - this.clientHeightStub = sinon - .stub(document.documentElement, 'clientHeight') - .get(function () { - return 1000; - }); - this.clientTopStub = sinon - .stub(document.documentElement, 'clientTop') - .get(function () { - return 0; - }); - }); - teardown(function () { - sharedTestTeardown.call(this); - }); - test('Below, in Bounds', function () { - const metrics = Blockly.DropDownDiv.TEST_ONLY.getPositionMetrics( - 50, - 0, - 50, - -10, - ); - // "Above" in value actually means below in render. - assert.isAtLeast(metrics.initialY, 0); - assert.isAbove(metrics.finalY, 0); - assert.isTrue(metrics.arrowVisible); - assert.isTrue(metrics.arrowAtTop); - }); - test('Above, in Bounds', function () { - const metrics = Blockly.DropDownDiv.TEST_ONLY.getPositionMetrics( - 50, - 100, - 50, - 90, - ); - // "Below" in value actually means above in render. - assert.isAtMost(metrics.initialY, 100); - assert.isBelow(metrics.finalY, 100); - assert.isTrue(metrics.arrowVisible); - assert.isFalse(metrics.arrowAtTop); - }); - test('Below, out of Bounds', function () { - const metrics = Blockly.DropDownDiv.TEST_ONLY.getPositionMetrics( - 50, - 60, - 50, - 50, - ); - // "Above" in value actually means below in render. - assert.isAtLeast(metrics.initialY, 60); - assert.isAbove(metrics.finalY, 60); - assert.isTrue(metrics.arrowVisible); - assert.isTrue(metrics.arrowAtTop); - }); - test('Above, in Bounds', function () { - const metrics = Blockly.DropDownDiv.TEST_ONLY.getPositionMetrics( - 50, - 100, - 50, - 90, - ); - // "Below" in value actually means above in render. - assert.isAtMost(metrics.initialY, 100); - assert.isBelow(metrics.finalY, 100); - assert.isTrue(metrics.arrowVisible); - assert.isFalse(metrics.arrowAtTop); - }); - test('No Solution, Render At Top', function () { - this.clientHeightStub.get(function () { - return 100; - }); - const metrics = Blockly.DropDownDiv.TEST_ONLY.getPositionMetrics( - 50, - 60, - 50, - 50, - ); - // "Above" in value actually means below in render. - assert.equal(metrics.initialY, 0); - assert.equal(metrics.finalY, 0); - assert.isFalse(metrics.arrowVisible); - assert.isNotOk(metrics.arrowAtTop); - }); - }); -}); diff --git a/tests/mocha/event_marker_move_test.js b/tests/mocha/event_marker_move_test.js deleted file mode 100644 index cd5609c33d7..00000000000 --- a/tests/mocha/event_marker_move_test.js +++ /dev/null @@ -1,44 +0,0 @@ -/** - * @license - * Copyright 2022 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import {assert} from '../../node_modules/chai/chai.js'; -import {defineRowBlock} from './test_helpers/block_definitions.js'; -import { - sharedTestSetup, - sharedTestTeardown, -} from './test_helpers/setup_teardown.js'; - -suite('Marker Move Event', function () { - setup(function () { - sharedTestSetup.call(this); - defineRowBlock(); - this.workspace = new Blockly.Workspace(); - }); - - teardown(function () { - sharedTestTeardown.call(this); - }); - - suite('Serialization', function () { - test('events round-trip through JSON', function () { - const block1 = this.workspace.newBlock('row_block', 'test_id1'); - const block2 = this.workspace.newBlock('row_block', 'test_id2'); - const node1 = new Blockly.ASTNode(Blockly.ASTNode.types.BLOCK, block1); - const node2 = new Blockly.ASTNode(Blockly.ASTNode.types.BLOCK, block2); - const origEvent = new Blockly.Events.MarkerMove( - block2, - false, - node1, - node2, - ); - - const json = origEvent.toJson(); - const newEvent = new Blockly.Events.fromJson(json, this.workspace); - - assert.deepEqual(newEvent, origEvent); - }); - }); -}); diff --git a/tests/mocha/field_textinput_test.js b/tests/mocha/field_textinput_test.js deleted file mode 100644 index 7170b27ff62..00000000000 --- a/tests/mocha/field_textinput_test.js +++ /dev/null @@ -1,297 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as Blockly from '../../build/src/core/blockly.js'; -import {assert} from '../../node_modules/chai/chai.js'; -import { - createTestBlock, - defineRowBlock, -} from './test_helpers/block_definitions.js'; -import { - assertFieldValue, - runConstructorSuiteTests, - runFromJsonSuiteTests, - runSetValueTests, -} from './test_helpers/fields.js'; -import { - sharedTestSetup, - sharedTestTeardown, - workspaceTeardown, -} from './test_helpers/setup_teardown.js'; - -suite('Text Input Fields', function () { - setup(function () { - sharedTestSetup.call(this); - }); - teardown(function () { - sharedTestTeardown.call(this); - }); - /** - * Configuration for field tests with invalid values. - * @type {!Array} - */ - const invalidValueTestCases = [ - {title: 'Undefined', value: undefined}, - {title: 'Null', value: null}, - ]; - /** - * Configuration for field tests with valid values. - * @type {!Array} - */ - const validValueTestCases = [ - {title: 'String', value: 'value', expectedValue: 'value'}, - {title: 'Boolean true', value: true, expectedValue: 'true'}, - {title: 'Boolean false', value: false, expectedValue: 'false'}, - {title: 'Number (Truthy)', value: 1, expectedValue: '1'}, - {title: 'Number (Falsy)', value: 0, expectedValue: '0'}, - {title: 'NaN', value: NaN, expectedValue: 'NaN'}, - ]; - const addArgsAndJson = function (testCase) { - testCase.args = [testCase.value]; - testCase.json = {'text': testCase.value}; - }; - invalidValueTestCases.forEach(addArgsAndJson); - validValueTestCases.forEach(addArgsAndJson); - - /** - * The expected default value for the field being tested. - * @type {*} - */ - const defaultFieldValue = ''; - /** - * Asserts that the field property values are set to default. - * @param {!Blockly.FieldTextInput} field The field to check. - */ - const assertFieldDefault = function (field) { - assertFieldValue(field, defaultFieldValue); - }; - /** - * Asserts that the field properties are correct based on the test case. - * @param {!Blockly.FieldTextInput} field The field to check. - * @param {!FieldValueTestCase} testCase The test case. - */ - const validTestCaseAssertField = function (field, testCase) { - assertFieldValue(field, testCase.expectedValue); - }; - - runConstructorSuiteTests( - Blockly.FieldTextInput, - validValueTestCases, - invalidValueTestCases, - validTestCaseAssertField, - assertFieldDefault, - ); - - runFromJsonSuiteTests( - Blockly.FieldTextInput, - validValueTestCases, - invalidValueTestCases, - validTestCaseAssertField, - assertFieldDefault, - ); - - suite('setValue', function () { - suite('Empty -> New Value', function () { - setup(function () { - this.field = new Blockly.FieldTextInput(); - }); - runSetValueTests( - validValueTestCases, - invalidValueTestCases, - defaultFieldValue, - ); - test('With source block', function () { - this.field.setSourceBlock(createTestBlock()); - this.field.setValue('value'); - assertFieldValue(this.field, 'value'); - }); - }); - suite('Value -> New Value', function () { - const initialValue = 'oldValue'; - setup(function () { - this.field = new Blockly.FieldTextInput(initialValue); - }); - runSetValueTests( - validValueTestCases, - invalidValueTestCases, - initialValue, - ); - test('With source block', function () { - this.field.setSourceBlock(createTestBlock()); - this.field.setValue('value'); - assertFieldValue(this.field, 'value'); - }); - }); - }); - - suite('Validators', function () { - setup(function () { - this.field = new Blockly.FieldTextInput('value'); - this.field.valueWhenEditorWasOpened_ = this.field.getValue(); - this.field.htmlInput_ = document.createElement('input'); - this.field.htmlInput_.setAttribute('data-old-value', 'value'); - this.field.htmlInput_.setAttribute('data-untyped-default-value', 'value'); - this.stub = sinon.stub(this.field, 'resizeEditor_'); - }); - teardown(function () { - sinon.restore(); - }); - const testSuites = [ - { - title: 'Null Validator', - validator: function () { - return null; - }, - value: 'newValue', - expectedValue: 'value', - }, - { - title: "Remove 'a' Validator", - validator: function (newValue) { - return newValue.replace(/a/g, ''); - }, - value: 'bbbaaa', - expectedValue: 'bbb', - }, - { - title: 'Returns Undefined Validator', - validator: function () {}, - value: 'newValue', - expectedValue: 'newValue', - expectedText: 'newValue', - }, - ]; - testSuites.forEach(function (suiteInfo) { - suite(suiteInfo.title, function () { - setup(function () { - this.field.setValidator(suiteInfo.validator); - }); - test('When Editing', function () { - this.field.isBeingEdited_ = true; - this.field.htmlInput_.value = suiteInfo.value; - this.field.onHtmlInputChange(null); - assertFieldValue( - this.field, - suiteInfo.expectedValue, - suiteInfo.value, - ); - }); - test('When Not Editing', function () { - this.field.setValue(suiteInfo.value); - assertFieldValue(this.field, suiteInfo.expectedValue); - }); - }); - }); - }); - - suite('Customization', function () { - suite('Spellcheck', function () { - setup(function () { - this.prepField = function (field) { - const workspace = { - getScale: function () { - return 1; - }, - getRenderer: function () { - return { - getClassName: function () { - return ''; - }, - }; - }, - getTheme: function () { - return { - getClassName: function () { - return ''; - }, - }; - }, - markFocused: function () {}, - options: {}, - }; - field.sourceBlock_ = { - workspace: workspace, - }; - field.constants_ = { - FIELD_TEXT_FONTSIZE: 11, - FIELD_TEXT_FONTWEIGHT: 'normal', - FIELD_TEXT_FONTFAMILY: 'sans-serif', - }; - field.clickTarget_ = document.createElement('div'); - Blockly.common.setMainWorkspace(workspace); - Blockly.WidgetDiv.createDom(); - this.stub = sinon.stub(field, 'resizeEditor_'); - }; - - this.assertSpellcheck = function (field, value) { - this.prepField(field); - field.showEditor_(); - assert.equal( - field.htmlInput_.getAttribute('spellcheck'), - value.toString(), - ); - }; - }); - teardown(function () { - if (this.stub) { - this.stub.restore(); - } - }); - test('Default', function () { - const field = new Blockly.FieldTextInput('test'); - this.assertSpellcheck(field, true); - }); - test('JS Constructor', function () { - const field = new Blockly.FieldTextInput('test', null, { - spellcheck: false, - }); - this.assertSpellcheck(field, false); - }); - test('JSON Definition', function () { - const field = Blockly.FieldTextInput.fromJson({ - text: 'test', - spellcheck: false, - }); - this.assertSpellcheck(field, false); - }); - test('setSpellcheck Editor Hidden', function () { - const field = new Blockly.FieldTextInput('test'); - field.setSpellcheck(false); - this.assertSpellcheck(field, false); - }); - test('setSpellcheck Editor Shown', function () { - const field = new Blockly.FieldTextInput('test'); - this.prepField(field); - field.showEditor_(); - field.setSpellcheck(false); - assert.equal(field.htmlInput_.getAttribute('spellcheck'), 'false'); - }); - }); - }); - - suite('Serialization', function () { - setup(function () { - this.workspace = new Blockly.Workspace(); - defineRowBlock(); - - this.assertValue = (value) => { - const block = this.workspace.newBlock('row_block'); - const field = new Blockly.FieldTextInput(value); - block.getInput('INPUT').appendField(field, 'TEXT'); - const jso = Blockly.serialization.blocks.save(block); - assert.deepEqual(jso['fields'], {'TEXT': value}); - }; - }); - - teardown(function () { - workspaceTeardown.call(this, this.workspace); - }); - - test('Simple', function () { - this.assertValue('test text'); - }); - }); -}); diff --git a/tests/mocha/insertion_marker_manager_test.js b/tests/mocha/insertion_marker_manager_test.js deleted file mode 100644 index 3fae888df38..00000000000 --- a/tests/mocha/insertion_marker_manager_test.js +++ /dev/null @@ -1,443 +0,0 @@ -/** - * @license - * Copyright 2022 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import {assert} from '../../node_modules/chai/chai.js'; -import { - defineRowBlock, - defineRowToStackBlock, - defineStackBlock, -} from './test_helpers/block_definitions.js'; -import { - sharedTestSetup, - sharedTestTeardown, -} from './test_helpers/setup_teardown.js'; - -suite('Insertion marker manager', function () { - setup(function () { - sharedTestSetup.call(this); - defineRowBlock(); - defineStackBlock(); - defineRowToStackBlock(); - this.workspace = Blockly.inject('blocklyDiv'); - }); - teardown(function () { - sharedTestTeardown.call(this); - }); - - suite('Creating markers', function () { - function createBlocksAndManager(workspace, state) { - Blockly.serialization.workspaces.load(state, workspace); - const block = workspace.getBlockById('first'); - const manager = new Blockly.InsertionMarkerManager(block); - return manager; - } - - test('One stack block creates one marker', function () { - const state = { - 'blocks': { - 'blocks': [ - { - 'type': 'stack_block', - 'id': 'first', - }, - ], - }, - }; - const manager = createBlocksAndManager(this.workspace, state); - const markers = manager.getInsertionMarkers(); - assert.equal(markers.length, 1); - }); - - test('Two stack blocks create two markers', function () { - const state = { - 'blocks': { - 'blocks': [ - { - 'type': 'stack_block', - 'id': 'first', - 'next': { - 'block': { - 'type': 'stack_block', - 'id': 'second', - }, - }, - }, - ], - }, - }; - const manager = createBlocksAndManager(this.workspace, state); - const markers = manager.getInsertionMarkers(); - assert.equal(markers.length, 2); - }); - - test('Three stack blocks create two markers', function () { - const state = { - 'blocks': { - 'blocks': [ - { - 'type': 'stack_block', - 'id': 'first', - 'next': { - 'block': { - 'type': 'stack_block', - 'id': 'second', - 'next': { - 'block': { - 'type': 'stack_block', - 'id': 'third', - }, - }, - }, - }, - }, - ], - }, - }; - const manager = createBlocksAndManager(this.workspace, state); - const markers = manager.getInsertionMarkers(); - assert.equal(markers.length, 2); - }); - - test('One value block creates one marker', function () { - const state = { - 'blocks': { - 'blocks': [ - { - 'type': 'row_block', - 'id': 'first', - }, - ], - }, - }; - const manager = createBlocksAndManager(this.workspace, state); - const markers = manager.getInsertionMarkers(); - assert.equal(markers.length, 1); - }); - - test('Two value blocks create one marker', function () { - const state = { - 'blocks': { - 'blocks': [ - { - 'type': 'row_block', - 'id': 'first', - 'inputs': { - 'INPUT': { - 'block': { - 'type': 'row_block', - 'id': 'second', - }, - }, - }, - }, - ], - }, - }; - const manager = createBlocksAndManager(this.workspace, state); - const markers = manager.getInsertionMarkers(); - assert.equal(markers.length, 1); - }); - - test('One row to stack block creates one marker', function () { - const state = { - 'blocks': { - 'blocks': [ - { - 'type': 'row_to_stack_block', - 'id': 'first', - }, - ], - }, - }; - const manager = createBlocksAndManager(this.workspace, state); - const markers = manager.getInsertionMarkers(); - assert.equal(markers.length, 1); - }); - - test('Row to stack block with child creates two markers', function () { - const state = { - 'blocks': { - 'blocks': [ - { - 'type': 'row_to_stack_block', - 'id': 'first', - 'next': { - 'block': { - 'type': 'stack_block', - 'id': 'second', - }, - }, - }, - ], - }, - }; - const manager = createBlocksAndManager(this.workspace, state); - const markers = manager.getInsertionMarkers(); - assert.equal(markers.length, 2); - }); - - suite('children being set as insertion markers', function () { - setup(function () { - Blockly.Blocks['shadows_in_init'] = { - init: function () { - this.appendValueInput('test').connection.setShadowState({ - 'type': 'math_number', - }); - this.setPreviousStatement(true); - }, - }; - - Blockly.Blocks['shadows_in_load'] = { - init: function () { - this.appendValueInput('test'); - this.setPreviousStatement(true); - }, - - loadExtraState: function () { - this.getInput('test').connection.setShadowState({ - 'type': 'math_number', - }); - }, - - saveExtraState: function () { - return true; - }, - }; - }); - - teardown(function () { - delete Blockly.Blocks['shadows_in_init']; - delete Blockly.Blocks['shadows_in_load']; - }); - - test('Shadows added in init are set as insertion markers', function () { - const state = { - 'blocks': { - 'blocks': [ - { - 'id': 'first', - 'type': 'shadows_in_init', - }, - ], - }, - }; - const manager = createBlocksAndManager(this.workspace, state); - const markers = manager.getInsertionMarkers(); - assert.isTrue( - markers[0].getChildren()[0].isInsertionMarker(), - 'Expected the shadow block to be an insertion maker', - ); - }); - - test('Shadows added in `loadExtraState` are set as insertion markers', function () { - const state = { - 'blocks': { - 'blocks': [ - { - 'id': 'first', - 'type': 'shadows_in_load', - }, - ], - }, - }; - const manager = createBlocksAndManager(this.workspace, state); - const markers = manager.getInsertionMarkers(); - assert.isTrue( - markers[0].getChildren()[0].isInsertionMarker(), - 'Expected the shadow block to be an insertion maker', - ); - }); - }); - }); - - suite('Would delete block', function () { - setup(function () { - const state = { - 'blocks': { - 'blocks': [ - { - 'type': 'stack_block', - 'id': 'first', - }, - ], - }, - }; - Blockly.serialization.workspaces.load(state, this.workspace); - this.block = this.workspace.getBlockById('first'); - this.manager = new Blockly.InsertionMarkerManager(this.block); - - const componentManager = this.workspace.getComponentManager(); - this.stub = sinon.stub(componentManager, 'hasCapability'); - this.dxy = new Blockly.utils.Coordinate(0, 0); - }); - - test('Over delete area and accepted would delete', function () { - this.stub - .withArgs( - 'fakeDragTarget', - Blockly.ComponentManager.Capability.DELETE_AREA, - ) - .returns(true); - const fakeDragTarget = { - wouldDelete: sinon.fake.returns(true), - id: 'fakeDragTarget', - }; - this.manager.update(this.dxy, fakeDragTarget); - assert.isTrue(this.manager.wouldDeleteBlock); - }); - - test('Over delete area and rejected would not delete', function () { - this.stub - .withArgs( - 'fakeDragTarget', - Blockly.ComponentManager.Capability.DELETE_AREA, - ) - .returns(true); - const fakeDragTarget = { - wouldDelete: sinon.fake.returns(false), - id: 'fakeDragTarget', - }; - this.manager.update(this.dxy, fakeDragTarget); - assert.isFalse(this.manager.wouldDeleteBlock); - }); - - test('Drag target is not a delete area would not delete', function () { - this.stub - .withArgs( - 'fakeDragTarget', - Blockly.ComponentManager.Capability.DELETE_AREA, - ) - .returns(false); - const fakeDragTarget = { - wouldDelete: sinon.fake.returns(false), - id: 'fakeDragTarget', - }; - this.manager.update(this.dxy, fakeDragTarget); - assert.isFalse(this.manager.wouldDeleteBlock); - }); - - test('Not over drag target would not delete', function () { - this.manager.update(this.dxy, null); - assert.isFalse(this.manager.wouldDeleteBlock); - }); - }); - - suite('Would connect stack blocks', function () { - setup(function () { - const state = { - 'blocks': { - 'blocks': [ - { - 'type': 'stack_block', - 'id': 'first', - 'x': 0, - 'y': 0, - }, - { - 'type': 'stack_block', - 'id': 'other', - 'x': 200, - 'y': 200, - }, - ], - }, - }; - Blockly.serialization.workspaces.load(state, this.workspace); - this.block = this.workspace.getBlockById('first'); - this.block.setDragging(true); - this.manager = new Blockly.InsertionMarkerManager(this.block); - }); - - test('No other blocks nearby would not connect', function () { - this.manager.update(new Blockly.utils.Coordinate(0, 0), null); - assert.isFalse(this.manager.wouldConnectBlock()); - }); - - test('Near other block and above would connect before', function () { - this.manager.update(new Blockly.utils.Coordinate(200, 190), null); - assert.isTrue(this.manager.wouldConnectBlock()); - const markers = this.manager.getInsertionMarkers(); - assert.equal(markers.length, 1); - const marker = markers[0]; - assert.isTrue(marker.nextConnection.isConnected()); - }); - - test('Near other block and below would connect after', function () { - this.manager.update(new Blockly.utils.Coordinate(200, 210), null); - assert.isTrue(this.manager.wouldConnectBlock()); - const markers = this.manager.getInsertionMarkers(); - assert.equal(markers.length, 1); - const marker = markers[0]; - assert.isTrue(marker.previousConnection.isConnected()); - }); - - test('Near other block and left would connect', function () { - this.manager.update(new Blockly.utils.Coordinate(190, 200), null); - assert.isTrue(this.manager.wouldConnectBlock()); - }); - - test('Near other block and right would connect', function () { - this.manager.update(new Blockly.utils.Coordinate(210, 200), null); - assert.isTrue(this.manager.wouldConnectBlock()); - }); - }); - - suite('Would connect row blocks', function () { - setup(function () { - const state = { - 'blocks': { - 'blocks': [ - { - 'type': 'row_block', - 'id': 'first', - 'x': 0, - 'y': 0, - }, - { - 'type': 'row_block', - 'id': 'other', - 'x': 200, - 'y': 200, - }, - ], - }, - }; - Blockly.serialization.workspaces.load(state, this.workspace); - this.block = this.workspace.getBlockById('first'); - this.block.setDragging(true); - this.manager = new Blockly.InsertionMarkerManager(this.block); - }); - - test('No other blocks nearby would not connect', function () { - this.manager.update(new Blockly.utils.Coordinate(0, 0), null); - assert.isFalse(this.manager.wouldConnectBlock()); - }); - - test('Near other block and above would connect', function () { - this.manager.update(new Blockly.utils.Coordinate(200, 190), null); - assert.isTrue(this.manager.wouldConnectBlock()); - }); - - test('Near other block and below would connect', function () { - this.manager.update(new Blockly.utils.Coordinate(200, 210), null); - assert.isTrue(this.manager.wouldConnectBlock()); - }); - - test('Near other block and left would connect before', function () { - this.manager.update(new Blockly.utils.Coordinate(190, 200), null); - assert.isTrue(this.manager.wouldConnectBlock()); - const markers = this.manager.getInsertionMarkers(); - assert.isTrue(markers[0].getInput('INPUT').connection.isConnected()); - }); - - test('Near other block and right would connect after', function () { - this.manager.update(new Blockly.utils.Coordinate(210, 200), null); - assert.isTrue(this.manager.wouldConnectBlock()); - const markers = this.manager.getInsertionMarkers(); - assert.isTrue(markers[0].outputConnection.isConnected()); - }); - }); -}); diff --git a/tests/mocha/test_helpers/icon_mocks.js b/tests/mocha/test_helpers/icon_mocks.js deleted file mode 100644 index 039c4082f5c..00000000000 --- a/tests/mocha/test_helpers/icon_mocks.js +++ /dev/null @@ -1,81 +0,0 @@ -/** - * @license - * Copyright 2023 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -export class MockIcon { - getType() { - return new Blockly.icons.IconType('mock icon'); - } - - initView() {} - - dispose() {} - - getWeight() {} - - getSize() { - return new Blockly.utils.Size(0, 0); - } - - applyColour() {} - - hideForInsertionMarker() {} - - updateEditable() {} - - updateCollapsed() {} - - isShownWhenCollapsed() {} - - setOffsetInBlock() {} - - onLocationChange() {} - - onClick() {} -} - -export class MockSerializableIcon extends MockIcon { - constructor() { - super(); - this.state = ''; - } - - getType() { - return new Blockly.icons.IconType('serializable icon'); - } - - getWeight() { - return 1; - } - - saveState() { - return 'some state'; - } - - loadState(state) { - this.state = state; - } -} - -export class MockBubbleIcon extends MockIcon { - constructor() { - super(); - this.visible = false; - } - - getType() { - return new Blockly.icons.IconType('bubble icon'); - } - - updateCollapsed() {} - - bubbleIsVisible() { - return this.visible; - } - - setBubbleVisible(visible) { - this.visible = visible; - } -} diff --git a/tests/node/run_node_test.mjs b/tests/node/run_node_test.mjs deleted file mode 100644 index ee95fd5282f..00000000000 --- a/tests/node/run_node_test.mjs +++ /dev/null @@ -1,72 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** @fileoverview Mocha tests that test Blockly in Node. */ - -console.log(process.cwd()); - -// N.B. the file ./node_modules/blockly-test should be a symlink to -// RELEASE_DIR (i.e. dist/) so that require will load the packaged -// version of blockly as if it were an external dependency. -// -// Moreover, (as with the typescript tests) this link has to be -// called something other than "blockly", because the node module -// resolution will favour loading the nearest enclosing package -// of the same name, which means that require('blockly') will load -// based on the exports section of the package.json in the repository -// root, but this fails because (at the time of writing) those paths -// are relative to RELEASE_DIR (dist/, into which package.json is -// copied when packaged), resulting in require() looking for the -// compressed bundles in the wrong place. - -import * as Blockly from 'blockly-test'; -import {javascriptGenerator} from 'blockly-test/javascript'; -import {assert} from 'chai'; - -const xmlText = - '\n' + - ' \n' + - ' \n' + - ' \n' + - ' Hello from Blockly!\n' + - ' \n' + - ' \n' + - ' \n' + - ''; - -suite('Test Node.js', function () { - test('Import XML', function () { - const xml = Blockly.utils.xml.textToDom(xmlText); - - // Create workspace and import the XML - const workspace = new Blockly.Workspace(); - Blockly.Xml.domToWorkspace(xml, workspace); - }); - test('Roundtrip XML', function () { - const xml = Blockly.utils.xml.textToDom(xmlText); - - const workspace = new Blockly.Workspace(); - Blockly.Xml.domToWorkspace(xml, workspace); - - const headlessXml = Blockly.Xml.workspaceToDom(workspace, true); - const headlessText = Blockly.Xml.domToPrettyText(headlessXml); - - assert.equal(headlessText, xmlText, 'equal'); - }); - test('Generate Code', function () { - const xml = Blockly.utils.xml.textToDom(xmlText); - - // Create workspace and import the XML - const workspace = new Blockly.Workspace(); - Blockly.Xml.domToWorkspace(xml, workspace); - - // Convert code - const code = javascriptGenerator.workspaceToCode(workspace); - - // Check output - assert.equal("window.alert('Hello from Blockly!');", code.trim(), 'equal'); - }); -}); diff --git a/tests/scripts/compile_typings.sh b/tests/scripts/compile_typings.sh deleted file mode 100755 index 35e9d56dc53..00000000000 --- a/tests/scripts/compile_typings.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash - -# Location that npm run typings will write .d.ts files to. -# -# (TODO(#5007): Should fetch this from scripts/gulpfiles/config.js -# instead of hardcoding it here. -readonly BUILD_DIR='build' - -# ANSI colors -BOLD_GREEN='\033[1;32m' -BOLD_RED='\033[1;31m' -ANSI_RESET='\033[0m' - -# Terminate immediately with non-zero status if any command exits -# with non-zero status, printing a nice message. -set -e -function fail { - echo -e "${BOLD_RED}Failed to compile TypeScript typings.${ANSI_RESET}" >&2 -} -trap fail ERR - -# Generate Blockly typings. -echo "Generating Blockly typings" -npm run typings - -# Use the TypeScript compiler to compile the generated typings. -echo "Compiling typings" - -cd "${BUILD_DIR}" -../node_modules/.bin/tsc blockly.d.ts - -echo -e "${BOLD_GREEN}TypeScript typings compiled successfully.${ANSI_RESET}"