[
  {
    "path": ".editorconfig",
    "content": "root = false\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 2\nend_of_line = lf\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": ".eslintignore",
    "content": "node_modules\n*.d.ts\nsrc/components/tools/paragraph\nsrc/polyfills.ts\n"
  },
  {
    "path": ".eslintrc",
    "content": "{\n  \"extends\": [\n    \"codex/ts\"\n  ],\n  \"globals\": {\n    \"Node\": true,\n    \"Range\": true,\n    \"HTMLElement\": true,\n    \"HTMLDivElement\": true,\n    \"Element\": true,\n    \"Selection\": true,\n    \"SVGElement\": true,\n    \"Text\": true,\n    \"InsertPosition\": true,\n    \"PropertyKey\": true,\n    \"MouseEvent\": true,\n    \"TouchEvent\": true,\n    \"KeyboardEvent\": true,\n    \"ClipboardEvent\": true,\n    \"DragEvent\": true,\n    \"Event\": true,\n    \"EventTarget\": true,\n    \"Document\": true,\n    \"NodeList\": true,\n    \"File\": true,\n    \"FileList\": true,\n    \"MutationRecord\": true,\n    \"AddEventListenerOptions\": true,\n    \"DataTransfer\": true,\n    \"DOMRect\": true,\n    \"ClientRect\": true,\n    \"ArrayLike\": true,\n    \"InputEvent\": true,\n    \"unknown\": true,\n    \"requestAnimationFrame\": true,\n    \"navigator\": true\n  },\n  \"rules\": {\n    \"jsdoc/require-returns-type\": \"off\",\n    \"@typescript-eslint/strict-boolean-expressions\": \"warn\",\n    \"@typescript-eslint/consistent-type-imports\": \"error\",\n    \"@typescript-eslint/consistent-type-exports\": \"error\"\n  },\n  \"overrides\": [\n    {\n      \"files\": [\n        \"tsconfig.json\",\n        \"package.json\",\n        \"tsconfig.*.json\",\n        \"tslint.json\"\n      ],\n      \"rules\": {\n        \"quotes\": [1, \"double\"],\n        \"semi\": [1, \"never\"],\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": ".github/CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, sex characteristics, gender identity and expression,\nlevel of experience, education, socio-economic status, nationality, personal\nappearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\n advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at team@codex.so. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see\nhttps://www.contributor-covenant.org/faq\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\npatreon: editorjs\nopen_collective: editorjs\ncustom: https://codex.so/donate"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve Editor.js\ntitle: \"\"\nlabels: bug\nassignees: ''\n\n---\n\nDescribe a bug.\n\nSteps to reproduce:\n1. Go to …\n2. Click on …\n3. …\n\nExpected behavior:\n\nScreenshots:\n\nDevice, Browser, OS:\n\nEditor.js version:\n\nPlugins you use with their versions:\n\n<!--\n🤫 If you like Editor.js, please consider supporting us via OpenCollective:\nhttps://opencollective.com/editorjs\n-->\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Team\n    url: mailto:team@codex.so\n    about: Direct team contact.\n  - name: 💬 Discussions\n    url: https://github.com/codex-team/editor.js/discussions\n    about: Use discussions if you have an issue draft, an idea for improvement or for asking questions.\n  - name: Editor.js Telegram chat\n    url: https://t.me/codex_editor\n    about: Telegram chat for Editor.js users communication."
  },
  {
    "path": ".github/ISSUE_TEMPLATE/general_issue.md",
    "content": "---\nname: General Issue\nabout: Well-designed, algorithmized feature/idea/improvement issue for Editor.js\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\nThe question.\n\nWhy and how the question has come up.\n\n<!--\n🤫 If you like Editor.js, please consider supporting us via OpenCollective:\nhttps://opencollective.com/editorjs\n-->\n"
  },
  {
    "path": ".github/workflows/bump-version-on-merge-next.yml",
    "content": "name: Bump version on merge\n\n# Caution:\n# the use of \"pull_request_target\" trigger allows to successfully\n# run workflow even when triggered from a fork. The trigger grants\n# access to repo's secrets and gives write permission to the runner.\n# This can be used to run malicious code on untrusted PR, so, please\n# DO NOT checkout any PR's ongoing commits (aka github.event.pull_request.head.sha)\n# while using this trigger.\non:\n  pull_request_target:\n    branches:\n      - next\n    types: [closed]\n\njobs:\n  # If pull request was merged then we should check for a package version update\n  check-for-no-version-changing:\n    if: github.event.pull_request.merged == true\n    runs-on: ubuntu-latest\n    permissions:\n      actions: write\n    steps:\n      # Checkout to target branch\n      - uses: actions/checkout@v2\n        with:\n          fetch-depth: 0\n\n      # Get package new version name\n      - name: Get package info\n        id: packageNew\n        uses: codex-team/action-nodejs-package-info@v1\n\n      # Checkout to the base commit before merge\n      - name: Checkout to the base commit before merge\n        run: git checkout ${{ github.event.pull_request.base.sha }}\n\n      # Get package old version name\n      - name: Get package info\n        id: packageOld\n        uses: codex-team/action-nodejs-package-info@v1\n\n      # Stop workflow and do not bump version if it was changed already\n      - name: Stop workflow if version was changed already\n        if: steps.packageOld.outputs.version != steps.packageNew.outputs.version\n        run: |\n          curl -L \\\n          -X POST \\\n          -H \"Accept: application/vnd.github+json\" \\\n          -H \"Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}\" \\\n          -H \"X-GitHub-Api-Version: 2022-11-28\" \\\n          https://api.github.com/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/cancel\n\n  bump-version:\n    needs: check-for-no-version-changing\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n      pull-requests: write\n    steps:\n      # Checkout to target branch\n      - uses: actions/checkout@v2\n\n      # Setup node environment\n      - uses: actions/setup-node@v1\n        with:\n          node-version: 16\n\n      # Bump version to the next prerelease (patch) with rc suffix\n      - name: Suggest the new version\n        run: yarn version --prerelease --preid rc --no-git-tag-version\n\n      # Get package new version name\n      - name: Get package info\n        id: package\n        uses: codex-team/action-nodejs-package-info@v1\n\n      # Create pull request with changes\n      - name: Create Pull Request\n        uses: peter-evans/create-pull-request@v3\n        with:\n          commit-message: Bump version\n          committer: github-actions <action@github.com>\n          author: github-actions <action@github.com>\n          branch: auto-bump-version\n          base: ${{ steps.vars.outputs.base_branch }}\n          delete-branch: true\n          title: \"Bump version up to ${{ steps.package.outputs.version }}\"\n          body: |\n            Auto-generated bump version suggestion because of PR:\n            **${{ github.event.pull_request.title }}** #${{ github.event.pull_request.number }}\n"
  },
  {
    "path": ".github/workflows/create-a-release-draft.yml",
    "content": "name: Create a release draft\n\n# Caution:\n# the use of \"pull_request_target\" trigger allows to successfully\n# run workflow even when triggered from a fork. The trigger grants\n# access to repo's secrets and gives write permission to the runner.\n# This can be used to run malicious code on untrusted PR, so, please\n# DO NOT checkout any PR's ongoing commits (aka github.event.pull_request.head.sha)\n# while using this trigger.\non:\n  pull_request_target:\n    branches:\n      - next\n    types: [closed]\n\njobs:\n  # If pull request was merged then we should check for a package version update\n  check-version-changing:\n    if: github.event.pull_request.merged == true\n    runs-on: ubuntu-latest\n    permissions:\n      actions: write\n    steps:\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 16\n      # Checkout to target branch\n      - uses: actions/checkout@v2\n        with:\n          fetch-depth: 0\n\n      # Get package new version name\n      - name: Get package info\n        id: packageNew\n        uses: codex-team/action-nodejs-package-info@v1\n\n      # Checkout to the base commit before merge\n      - name: Checkout to the base commit before merge\n        run: git checkout ${{ github.event.pull_request.base.sha }}\n\n      # Get package old version name\n      - name: Get package info\n        id: packageOld\n        uses: codex-team/action-nodejs-package-info@v1\n\n      # Stop workflow if version was not changed\n      - name: Stop workflow if version was not changed\n        if: steps.packageOld.outputs.version == steps.packageNew.outputs.version\n        run: |\n          curl -L \\\n          -X POST \\\n          -H \"Accept: application/vnd.github+json\" \\\n          -H \"Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}\" \\\n          -H \"X-GitHub-Api-Version: 2022-11-28\" \\\n          https://api.github.com/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/cancel\n\n  # Create a new draft release\n  release-draft:\n    needs: check-version-changing\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n    steps:\n      # Checkout to target branch\n      - uses: actions/checkout@v2\n        with:\n          # Pull submodules\n          submodules: 'recursive'\n\n      # Setup node environment\n      - uses: actions/setup-node@v1\n        with:\n          node-version: 16\n\n      # Prepare, build and publish project\n      - name: Install dependencies\n        run: yarn\n\n      # Build Editor.js\n      - name: Build output files\n        run: yarn build\n\n      # Get package version name\n      - name: Get package info\n        id: package\n        uses: codex-team/action-nodejs-package-info@v1\n\n      - name: Create Release\n        id: create_release\n        uses: actions/create-release@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          tag_name: v${{ steps.package.outputs.version }}\n          release_name: v${{ steps.package.outputs.version }}\n\n          # Fill release description from pull request body name\n          body: \"${{ github.event.pull_request.title }} #${{ github.event.pull_request.number }}\"\n\n          # Save as a draft release\n          draft: true\n\n          # If version name contains \"-rc\" suffix than mark a \"pre-release\" checkbox\n          prerelease: ${{ contains(steps.package.outputs.version, '-rc') }}\n\n      # Build and upload target Editor.js UMD build to release as artifact\n      - name: Upload Release Asset\n        uses: actions/upload-release-asset@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ steps.create_release.outputs.upload_url }}\n          asset_path: dist/editorjs.umd.js\n          asset_name: editorjs.umd.js\n          asset_content_type: application/javascript\n\n      # Build and upload target Editor.js MJS build to release as artifact\n      - name: Upload Release Asset\n        uses: actions/upload-release-asset@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ steps.create_release.outputs.upload_url }}\n          asset_path: dist/editorjs.mjs\n          asset_name: editorjs.mjs\n          asset_content_type: application/javascript\n\n      # Send a notification message\n      - name: Send a message\n        uses: codex-team/action-codexbot-notify@v1\n        with:\n          webhook: ${{ secrets.CODEX_BOT_WEBHOOK_FRONTEND }}\n          message: '🦥 [Draft release v${{ steps.package.outputs.version }}](${{ steps.create_release.outputs.html_url }}) for package [${{ steps.package.outputs.name }}](${{ steps.package.outputs.npmjs-link }}) has been created. Add changelog and publish it!'\n          parse_mode: 'markdown'\n          disable_web_page_preview: true\n"
  },
  {
    "path": ".github/workflows/cypress.yml",
    "content": "name: Cypress\n\non: [pull_request]\n\njobs:\n  run-tests:\n    strategy:\n      matrix:\n        browser: [firefox, chrome, edge]\n\n    runs-on: ubuntu-22.04\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 18\n\n      - name: Setup Firefox\n        if: matrix.browser == 'firefox'\n        uses: browser-actions/setup-firefox@v1\n        with:\n          firefox-version: '115.0esr'\n\n      - uses: cypress-io/github-action@v6\n        with:\n          config: video=false\n          browser: ${{ matrix.browser }}\n          build: yarn build:test\n"
  },
  {
    "path": ".github/workflows/eslint.yml",
    "content": "name: ESLint CodeX\n\non: [pull_request]\n\njobs:\n  lint:\n    name: ESlint\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 18\n\n      - name: Cache dependencies\n        uses: actions/cache@v4\n        with:\n          path: ~/.npm\n          key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}\n          restore-keys: |\n            ${{ runner.os }}-node-\n\n      - run: yarn\n      - run: yarn lint\n"
  },
  {
    "path": ".github/workflows/publish-package-to-npm.yml",
    "content": "name: Publish package to NPM\n\non:\n  release:\n    types:\n      - published\n\njobs:\n  publish:\n    runs-on: ubuntu-latest\n    steps:\n      # Checkout to target branch\n      - uses: actions/checkout@v4\n        with:\n          # Pull submodules\n          submodules: 'recursive'\n\n      - name: Get package info\n        id: package\n        uses: codex-team/action-nodejs-package-info@v1\n\n      # Setup node environment\n      - uses: actions/setup-node@v1\n        with:\n          node-version: 16\n          registry-url: https://registry.npmjs.org/\n\n      # Prepare, build and publish project\n      - name: Install dependencies\n        run: yarn\n\n      - name: Build output files\n        run: yarn build\n\n      - name: Publish the package with a NEXT tag\n        run: yarn publish --access=public --tag=next\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n\n      - name: Add LATEST tag for the published package if this is not a prerelease version\n        if: github.event.release.prerelease != true\n        run: npm dist-tag add ${{ steps.package.outputs.name }}@${{ steps.package.outputs.version }} latest\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n\n  notify:\n    needs: publish\n    runs-on: ubuntu-latest\n    env:\n      GITHUB_LINK: ${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ github.ref_name }}\n    steps:\n      # Checkout to target branch\n      - uses: actions/checkout@v4\n\n      - name: Get package info\n        id: package\n        uses: codex-team/action-nodejs-package-info@v1\n\n      - name: Send a message\n        uses: codex-team/action-codexbot-notify@v1\n        with:\n          webhook: ${{ secrets.CODEX_BOT_NOTIFY_EDITORJS_PUBLIC_CHAT }}\n          message: '📦 [${{ steps.package.outputs.name }} ${{ steps.package.outputs.version }}](${{ env.GITHUB_LINK }}) was published'\n          parse_mode: 'markdown'\n          disable_web_page_preview: true\n"
  },
  {
    "path": ".gitignore",
    "content": "# --- proj files ---\n.DS_Store\nThumbs.db\n/.idea/\n/*.sublime-project\n/*.sublime-workspace\n\nnode_modules/*\n\nnpm-debug.log\nyarn-error.log\n\ntest/cypress/screenshots\ntest/cypress/videos\n\ndist/\n\ncoverage/\n.nyc_output/\n.vscode/launch.json\n"
  },
  {
    "path": ".npmignore",
    "content": "*\n!/dist/**/*\n!/types/**/*\n!/LICENSE\n!/README.md\n!/package.json\n"
  },
  {
    "path": ".nvmrc",
    "content": "v18.20.1\n"
  },
  {
    "path": ".postcssrc.yml",
    "content": "plugins:\n  # Apply custom property sets via @apply rule\n  # https://github.com/pascalduez/postcss-apply\n  postcss-apply: {}\n\n  # Convert modern CSS into something most browsers can understand\n  # https://github.com/csstools/postcss-preset-env\n  postcss-preset-env:\n    # Polyfill CSS features\n    # https://github.com/csstools/postcss-preset-env#stage\n    #\n    # List of features with levels: https://cssdb.org/\n    stage: 0\n\n    # Define polyfills based on browsers you are supporting\n    # https://github.com/csstools/postcss-preset-env#browsers\n    browsers:\n      - 'last 2 versions'\n      - '> 1%'\n\n    # Instruct all plugins to omit pre-polyfilled CSS\n    # https://github.com/csstools/postcss-preset-env#preserve\n    preserve: false\n\n  # Nested rules unwrapper\n  # https://github.com/postcss/postcss-nested\n  #\n  # As you know 'postcss-preset-env' plugin has an ability to process\n  # 'postcss-nesting' feature but it does not work with BEM\n  # Report: https://github.com/csstools/postcss-preset-env/issues/40\n  postcss-nested: {}\n"
  },
  {
    "path": ".stylelintrc",
    "content": "{\n  \"rules\": {\n    \"at-rule-empty-line-before\": [\n      \"always\",\n      {\n        except: [\n          \"blockless-after-same-name-blockless\",\n          \"first-nested\",\n        ],\n        ignore: [\n          \"after-comment\"\n        ],\n      }\n    ],\n    \"at-rule-name-case\": \"lower\",\n    \"at-rule-name-space-after\": \"always-single-line\",\n    \"at-rule-semicolon-newline-after\": \"always\",\n    \"block-closing-brace-empty-line-before\": \"never\",\n    \"block-closing-brace-newline-after\": \"always\",\n    \"block-closing-brace-newline-before\": \"always-multi-line\",\n    \"block-closing-brace-space-before\": \"always-single-line\",\n    \"block-no-empty\": true,\n    \"block-opening-brace-newline-after\": \"always-multi-line\",\n    \"block-opening-brace-space-after\": \"always-single-line\",\n    \"block-opening-brace-space-before\": \"always\",\n    \"color-hex-case\": \"lower\",\n    \"color-hex-length\": \"short\",\n    \"color-no-invalid-hex\": true,\n    \"comment-empty-line-before\": [\n      \"always\",\n      {\n        except: [\n          \"first-nested\"\n        ],\n        ignore: [\n          \"stylelint-commands\"\n        ],\n      }\n    ],\n    \"comment-no-empty\": true,\n    \"comment-whitespace-inside\": \"always\",\n    \"custom-property-empty-line-before\": [\n      \"always\",\n      {\n        except: [\n          \"after-custom-property\",\n          \"first-nested\",\n        ],\n        ignore: [\n          \"after-comment\",\n          \"inside-single-line-block\",\n        ],\n      }\n    ],\n    \"declaration-bang-space-after\": \"never\",\n    \"declaration-bang-space-before\": \"always\",\n    \"declaration-block-no-duplicate-properties\": [\n      true,\n      {\n        ignore: [\n          \"consecutive-duplicates-with-different-values\"\n        ],\n      }\n    ],\n    \"declaration-block-no-redundant-longhand-properties\": true,\n    \"declaration-block-no-shorthand-property-overrides\": true,\n    \"declaration-block-semicolon-newline-after\": \"always-multi-line\",\n    \"declaration-block-semicolon-space-after\": \"always-single-line\",\n    \"declaration-block-semicolon-space-before\": \"never\",\n    \"declaration-block-single-line-max-declarations\": 1,\n    \"declaration-block-trailing-semicolon\": \"always\",\n    \"declaration-colon-newline-after\": \"always-multi-line\",\n    \"declaration-colon-space-after\": \"always-single-line\",\n    \"declaration-colon-space-before\": \"never\",\n    \"declaration-empty-line-before\": [\n      \"always\",\n      {\n        except: [\n          \"after-declaration\",\n          \"first-nested\",\n        ],\n        ignore: [\n          \"after-comment\",\n          \"inside-single-line-block\",\n        ],\n      }\n    ],\n    \"font-family-no-duplicate-names\": true,\n    \"function-calc-no-unspaced-operator\": true,\n    \"function-comma-newline-after\": \"always-multi-line\",\n    \"function-comma-space-after\": \"always-single-line\",\n    \"function-comma-space-before\": \"never\",\n    \"function-linear-gradient-no-nonstandard-direction\": true,\n    \"function-max-empty-lines\": 0,\n    \"function-name-case\": \"lower\",\n    \"function-parentheses-newline-inside\": \"always-multi-line\",\n    \"function-parentheses-space-inside\": \"never-single-line\",\n    \"function-whitespace-after\": \"always\",\n    \"indentation\": 4,\n    \"keyframe-declaration-no-important\": true,\n    \"length-zero-no-unit\": true,\n    \"max-empty-lines\": 1,\n    \"media-feature-colon-space-after\": \"always\",\n    \"media-feature-colon-space-before\": \"never\",\n    \"media-feature-name-case\": \"lower\",\n    \"media-feature-name-no-unknown\": true,\n    \"media-feature-parentheses-space-inside\": \"never\",\n    \"media-feature-range-operator-space-after\": \"always\",\n    \"media-feature-range-operator-space-before\": \"always\",\n    \"media-query-list-comma-newline-after\": \"always-multi-line\",\n    \"media-query-list-comma-space-after\": \"always-single-line\",\n    \"media-query-list-comma-space-before\": \"never\",\n    \"no-empty-source\": true,\n    \"no-eol-whitespace\": true,\n    \"no-extra-semicolons\": true,\n    \"no-invalid-double-slash-comments\": true,\n    \"no-missing-end-of-source-newline\": true,\n    \"number-leading-zero\": \"always\",\n    \"number-no-trailing-zeros\": true,\n    \"property-case\": \"lower\",\n    \"property-no-unknown\": true,\n    \"rule-empty-line-before\": [\n      \"always-multi-line\",\n      {\n        except: [\n          \"first-nested\"\n        ],\n        ignore: [\n          \"after-comment\"\n        ],\n      }\n    ],\n    \"selector-attribute-brackets-space-inside\": \"never\",\n    \"selector-attribute-operator-space-after\": \"never\",\n    \"selector-attribute-operator-space-before\": \"never\",\n    \"selector-combinator-space-after\": \"always\",\n    \"selector-combinator-space-before\": \"always\",\n    \"selector-descendant-combinator-no-non-space\": true,\n    \"selector-list-comma-newline-after\": \"always\",\n    \"selector-list-comma-space-before\": \"never\",\n    \"selector-max-empty-lines\": 0,\n    \"selector-pseudo-class-case\": \"lower\",\n    \"selector-pseudo-class-no-unknown\": true,\n    \"selector-pseudo-class-parentheses-space-inside\": \"never\",\n    \"selector-pseudo-element-case\": \"lower\",\n    \"selector-pseudo-element-colon-notation\": \"double\",\n    \"selector-pseudo-element-no-unknown\": true,\n    \"selector-type-case\": \"lower\",\n    \"selector-type-no-unknown\": true,\n    \"shorthand-property-no-redundant-values\": true,\n    \"string-no-newline\": true,\n    \"unit-case\": \"lower\",\n    \"unit-no-unknown\": true,\n    \"value-list-comma-newline-after\": \"always-multi-line\",\n    \"value-list-comma-space-after\": \"always-single-line\",\n    \"value-list-comma-space-before\": \"never\",\n    \"value-list-max-empty-lines\": 0,\n  },\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n    \"cSpell.words\": [\n      \"autofocused\",\n      \"Behaviour\",\n      \"cacheable\",\n      \"childs\",\n      \"codexteam\",\n      \"colspan\",\n      \"contenteditable\",\n      \"contentless\",\n      \"Convertable\",\n      \"cssnano\",\n      \"cssnext\",\n      \"Debouncer\",\n      \"devserver\",\n      \"editorjs\",\n      \"entrypoints\",\n      \"Flippable\",\n      \"GRAMMARLY\",\n      \"hsablonniere\",\n      \"intellij\",\n      \"keydown\",\n      \"keydowns\",\n      \"Kilian\",\n      \"mergeable\",\n      \"movetostart\",\n      \"nofollow\",\n      \"opencollective\",\n      \"preconfigured\",\n      \"resetors\",\n      \"rowspan\",\n      \"selectall\",\n      \"sometool\",\n      \"stylelint\",\n      \"textareas\",\n      \"twitterwidget\",\n      \"typeof\",\n      \"Unmergeable\",\n      \"viewports\"\n    ]\n}\n"
  },
  {
    "path": "CODEOWNERS",
    "content": "*        @neSpecc @gohabereg @TatianaFomina @ilyamore88\n\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   Copyright © 2015-present CodeX\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n  <a href=\"https://editorjs.io/\">\n    <picture>\n      <source media=\"(prefers-color-scheme: dark)\"  srcset=\"./assets/logo_night.png\">\n      <source media=\"(prefers-color-scheme: light)\" srcset=\"./assets/logo_day.png\">\n      <img alt=\"Editor.js Logo\" src=\"./assets/logo_day.png\">\n    </picture>    \n  </a>\n</p>\n\n<p align=\"center\">\n <a href=\"https://editorjs.io/\">editorjs.io</a> |\n  <a href=\"https://editorjs.io/base-concepts/\">documentation</a> |\n  <a href=\"https://github.com/codex-team/editor.js/blob/next/docs/CHANGELOG.md\">changelog</a>\n  \n</p>\n\n<p align=\"center\">\n  <a href=\"https://www.npmjs.com/package/@editorjs/editorjs\">\n    <img src=\"https://flat.badgen.net/npm/v/@editorjs/editorjs?icon=npm\" alt=\"npm\"/>\n  </a>\n  <a href=\"https://www.npmjs.com/package/@editorjs/editorjs\">\n    <img src=\"https://flat.badgen.net/bundlephobia/minzip/@editorjs/editorjs?color=green\" alt=\"Minzipped size\"/>\n  </a>\n  <a href=\"https://github.com/codex-team/editor.js#backers\">\n    <img src=\"https://opencollective.com/editorjs/backers/badge.svg\" alt=\"Backers on Open Collective\"/>\n  </a>\n  <a href=\"https://github.com/codex-team/editor.js#sponsors\">\n    <img src=\"https://opencollective.com/editorjs/sponsors/badge.svg\" alt=\"Sponsors on Open Collective\"/>\n  </a>\n</p>\n\n## About\n\nEditor.js is an open-source text editor offering a variety of features to help users create and format content efficiently. It has a modern, block-style interface that allows users to easily add and arrange different types of content, such as text, images, lists, quotes, etc. Each Block is provided via a separate plugin making Editor.js extremely flexible.\n\nEditor.js outputs a clean JSON data instead of heavy HTML markup. Use it in Web, iOS, Android, AMP, Instant Articles, speech readers, AI chatbots — everywhere. Easy to sanitize, extend and integrate with your logic. \n\n- 😍  Modern UI out of the box\n- 💎  Clean JSON output\n- ⚙️  Well-designed API\n- 🛍  Various Tools available\n- 💌  Free and open source\n\n<picture>\n  <img alt=\"Editor.js Overview\" src=\"./assets/overview.png\">\n</picture>   \n\n## Installation\n\nIt's quite simple:\n\n1. Install Editor.js \n2. Install tools you need\n3. Initialize Editor's instance\n\nInstall using NPM, Yarn, or [CDN](https://www.jsdelivr.com/package/npm/@editorjs/editorjs):\n\n```bash\nnpm i @editorjs/editorjs\n```\n\nChoose and install tools:\n\n- [Heading](https://github.com/editor-js/header)\n- [Quote](https://github.com/editor-js/quote)\n- [Image](https://github.com/editor-js/image) \n- [Simple Image](https://github.com/editor-js/simple-image) (without backend requirement)\n- [Nested List](https://github.com/editor-js/nested-list)\n- [Checklist](https://github.com/editor-js/checklist)\n- [Link embed](https://github.com/editor-js/link)\n- [Embeds](https://github.com/editor-js/embed) (YouTube, Twitch, Vimeo, Gfycat, Instagram, Twitter, etc)\n- [Table](https://github.com/editor-js/table)\n- [Delimiter](https://github.com/editor-js/delimiter)\n- [Warning](https://github.com/editor-js/warning)\n- [Code](https://github.com/editor-js/code)\n- [Raw HTML](https://github.com/editor-js/raw)\n- [Attaches](https://github.com/editor-js/attaches)\n- [Marker](https://github.com/editor-js/marker)\n- [Inline Code](https://github.com/editor-js/inline-code)\n\nSee the [😎 Awesome Editor.js](https://github.com/editor-js/awesome-editorjs) list for more tools.\n\nInitialize the Editor:\n\n```html\n<div id=\"editorjs\"></div>\n```\n\n```javascript\nimport EditorJS from '@editorjs/editorjs'\n\nconst editor = new EditorJS({\n  tools: {\n   // ... your tools\n  }\n})\n````\n\nSee details about [Installation](https://editorjs.io/getting-started/) and [Configuration](https://editorjs.io/configuration/) at the documentation.\n\n### Saving Data\n\nCall `editor.save()` and handle returned Promise with saved data.\n\n```javascript\nconst data = await editor.save()\n```\n\n### Example\n\nTake a look at the [example.html](example/example.html) to view more detailed examples.\n\n\n## Roadmap\n\n<img align=\"right\" width=\"342\" src=\"./assets/roadmap.png\" style=\"margin-left: 30px\">\n\n- Unified Toolbars\n  - [x] Block Tunes moved left\n  - [x] Toolbox becomes vertical\n  - [x] Ability to display several Toolbox buttons by the single Tool\n  - [x] Block Tunes become vertical\n  - [x] Block Tunes support nested menus\n  - [x] Block Tunes support separators \n  - [x] Conversion Menu added to the Block Tunes\n  - [x] Unified Toolbar supports hints \n  - [x] Conversion Toolbar uses Unified Toolbar\n  - [x] Inline Toolbar uses Unified Toolbar\n- Collaborative editing\n  - [ ] Implement Inline Tools JSON format\n  - [ ] Operations Observer, Executor, Manager, Transformer\n  - [ ] Implement Undo/Redo Manager\n  - [ ] Implement Tools API changes\n  - [ ] Implement Server and communication\n  - [ ] Update basic tools to fit the new API\n- Other features\n  - [ ] Blocks drag'n'drop\n  - [ ] New cross-block selection\n  - [ ] New cross-block caret moving\n- Ecosystem improvements\n  - [x] CodeX Icons — the way to unify all tools and core icons\n  - [x] New Homepage and Docs\n  - [x] @editorjs/create-tool for Tools bootstrapping\n  - [ ] Editor.js DevTools — stand for core and tools development\n  - [ ] Editor.js Design System\n  - [ ] Editor.js Preset Env\n  - [ ] Editor.js ToolKit\n  - [ ] New core bundle system\n  - [ ] New documentation and guides\n\n<a href=\"https://opencollective.com/editorjs/donate\" target=\"_blank\">\n  <picture>\n    <source width=\"162px\" media=\"(prefers-color-scheme: dark)\"  srcset=\"./assets/support_night.png\">\n    <source width=\"162px\" media=\"(prefers-color-scheme: light)\" srcset=\"./assets/support_day.png\">\n    <img width=\"162px\" alt=\"Support Editor.js\" src=\"./assets/support_day.png\">\n  </picture>\n</a>\n\n<br>\n\n## Like Editor.js?\n\nYou can support project improvement and development of new features with a donation to our team.\n\n[Donate via OpenCollective](https://opencollective.com/editorjs)\n\\\n[Donate via Crypto](https://codex.so/donate)\n\\\n[Donate via Patreon](https://www.patreon.com/editorjs)\n\n### Why donate\n\nDonations to open-source products have several advantages for your business:\n\n- If your business relies on Editor.js, you'll probably want it to be maintained\n- It helps Editor.js to evolve and get the new features\n- We can support contributors and the community around the project. You'll receive well organized docs, guides, etc.\n- We need to pay for our infrastructure and maintain public resources (domain names, homepages, docs, etc). Supporting it guarantees you to access any resources at the time you need them.\n- You can advertise by adding your brand assets and mentions on our public resources\n\n\n### Sponsors\n\nSupport us by becoming a sponsor. Your logo will show up here with a link to your website.\n\n<p>\n  <a href=\"https://www.mister-auto.com/\" target=\"_blank\">\n    <img src=\"https://opencollective-production.s3.us-west-1.amazonaws.com/5131a030-5672-11ec-be79-1d003d12ec5f.png\" width=\"50\" alt=\"Mister Auto\">\n  </a>\n  <a href=\"https://www.uplucid.com/\" target=\"_blank\">\n    <img src=\"https://logo.clearbit.com/uplucid.com\" width=\"50\" alt=\"UPLUCID, K.K.\">\n  </a>\n  <a href=\"https://www.contentharmony.com/\" target=\"_blank\">\n    <img src=\"https://opencollective-production.s3.us-west-1.amazonaws.com/89edb1b0-7d82-11ed-b99e-ab6e6f9cb69f.png\" width=\"50\" alt=\"Kane Jamison\">\n  </a>\n  <a href=\"https://www.contentharmony.com/product/\" target=\"_blank\">\n    <img src=\"https://logo.clearbit.com/contentharmony.com\" width=\"50\" alt=\"Content Harmony\">\n  </a>\n</p>\n\n[Become a Sponsor](https://opencollective.com/editorjs/contribute/sir-8679/checkout)\n\n### Backers\n Thank you to all our backers\n\n<a href=\"https://opencollective.com/editorjs#backers\" target=\"_blank\"><img src=\"https://opencollective.com/editorjs/backers.svg?width=890&avatarHeight=34\"></a>\n\n[Become a Backer](https://opencollective.com/editorjs/contribute/backer-8632/checkout)\n\n### Contributors\n\nThis project exists thanks to all the people who contribute. \n\n<p><img src=\"https://opencollective.com/editorjs/contributors.svg?width=890&button=false&avatarHeight=34\" /></p>\n\n### Need something special?\n\nHire CodeX experts to resolve technical challenges and match your product requirements. \n\n- Resolve a problem that has high value for you\n- Implement a new feature required by your business\n- Help with integration or tool development\n- Provide any consultation\n\nContact us via team@codex.so and share your details\n\n## Community\n\n- [Official Tools](https://github.com/editor-js)\n- [Awesome Editor.js](https://github.com/editor-js/awesome-editorjs)\n- [Good First Tasks](https://github.com/codex-team/editor.js/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+task%22)\n- [Contributing](https://editorjs.io/contributing/)\n- [Telegram Chat](https://t.me/codex_editor)\n\n# About CodeX\n\n<img align=\"right\" width=\"120\" height=\"120\" src=\"https://codex.so/public/app/img/codex-logo.svg\" hspace=\"50\">\n\nCodeX is a team of digital specialists around the world interested in building high-quality open source products on a global market. We are [open](https://codex.so/join) for young people who want to constantly improve their skills and grow professionally with experiments in cutting-edge technologies.\n\n| 🌐 | Join  👋  | Twitter | Instagram |\n| -- | -- | -- | -- |\n| [codex.so](https://codex.so) | [codex.so/join](https://codex.so/join) |[@codex_team](http://twitter.com/codex_team) | [@codex_team](http://instagram.com/codex_team/) |\n"
  },
  {
    "path": "cypress.config.ts",
    "content": "import { defineConfig } from 'cypress';\nimport path from 'node:path';\nimport vitePreprocessor from 'cypress-vite';\n\nexport default defineConfig({\n  env: {\n    NODE_ENV: 'test',\n  },\n  fixturesFolder: 'test/cypress/fixtures',\n  screenshotsFolder: 'test/cypress/screenshots',\n  video: false,\n  videosFolder: 'test/cypress/videos',\n  e2e: {\n    // We've imported your old cypress plugins here.\n    // You may want to clean this up later by importing these.\n    setupNodeEvents(on, config) {\n      on('file:preprocessor', vitePreprocessor({\n        configFile: path.resolve(__dirname, './vite.config.test.js'),\n      }));\n\n      /**\n       * Plugin for cypress that adds better terminal output for easier debugging.\n       * Prints cy commands, browser console logs, cy.request and cy.intercept data. Great for your pipelines.\n       * https://github.com/archfz/cypress-terminal-report\n       */\n      require('cypress-terminal-report/src/installLogsPrinter')(on);\n\n      require('@cypress/code-coverage/task')(on, config);\n    },\n    specPattern: 'test/cypress/tests/**/*.cy.{js,jsx,ts,tsx}',\n    supportFile: 'test/cypress/support/index.ts',\n  },\n  'retries': {\n    // Configure retry attempts for `cypress run`\n    'runMode': 2,\n    // Configure retry attempts for `cypress open`\n    'openMode': 0,\n  },\n});\n"
  },
  {
    "path": "docs/CHANGELOG.md",
    "content": "# Changelog\n\n### 2.31.5\n\n- `Fix` - Handle __Ctrl + click__ on links with inline styles applied (e.g., bold, italic)\n\n### 2.31.4\n\n- `Fix` - Prevent inline-toolbar re-renders when linked text is selected\n\n### 2.31.3\n\n- `Fix` - Prevent text formatting removal when applying link\n\n### 2.31.2\n\n- `Fix` - Prevent link removal when applying bold to linked text\n\n### 2.31.1\n\n- `Fix` - Prevent the warning from appearing when `readOnly` mode is initially set to `true`\n\n### 2.31.0\n\n- `New` - Inline tools (those with `isReadOnlySupported` specified) can now be used in read-only mode\n- `New` - Inline tools (those with `isReadOnlySupported` specified) shortcuts now work in read-only mode\n- `Improvement` - Block manager passes target tool config to the `conversionConfig.import` method on conversion\n- `Fix` - Fix selection of first block in read-only initialization with \"autofocus=true\"\n- `Fix` - Incorrect caret position after blocks merging in Safari\n- `Fix` - Several toolbox items exported by the one tool have the same shortcut displayed in toolbox\n- `Improvement` - The current block reference will be updated in read-only mode when blocks are clicked\n- `Fix` - codex-notifier and codex-tooltip moved from devDependencies to dependencies in package.json to solve type errors\n- `Fix` - Handle whitespace input in empty placeholder elements to prevent caret from moving unexpectedly to the end of the placeholder\n- `Fix` - Fix the memory leak issue in `Shortcuts` class\n- `Fix` - Fix when / overides selected text outside of the editor\n- `DX` - Tools submodules removed from the repository\n- `Improvement` - Shift + Down/Up will allow to select next/previous line instead of Inline Toolbar flipping\n- `Improvement` - The API `caret.setToBlock()` offset now works across the entire block content, not just the first or last node.\n- `Improvement` - The API `blocks.renderFromHTML()` became async and now can be awaited.\n- `Fix` - `blocks.renderFromHTML()` — Error  \"Can't find a Block to remove.\" fixed\n- `Fix` - The API `.clear()` index invalidation fixed\n\n\n\n### 2.30.7\n\n- `Fix` - Link insertion in Safari fixed\n\n### 2.30.6\n\n- `Fix` – Fix the display of ‘Convert To’ near blocks that do not have the ‘conversionConfig.export’ rule specified\n- `Fix` – The Plus button does not appear when the editor is loaded in an iframe in Chrome\n- `Fix` - Prevent inline toolbar from closing in nested instance of editor\n\n### 2.30.5\n\n– `Fix` – Fix exported types\n\n### 2.30.4\n\n- `Fix` - Tool's exporting types added\n\n### 2.30.3\n\n- `Fix` – I18n in nested popover\n\n### 2.30.2\n\n- `Fix` – The onChange callback won't be fired when editor is initialized in the Read-Only mode\n- `Fix` – Convert To supports i18n again\n- `Fix` – Prevent form submit on inline tool click\n\n### 2.30.1\n\n- `Fix` – Remove fake selection after multiple \"convert to\" inline tool toggles\n\n### 2.30.0\n\n- `New` – Block Tunes now supports nesting items\n- `New` – Block Tunes now supports separator items\n- `New` – *Menu Config* – New item type – HTML\n- `New` – *Menu Config* – Default and HTML items now support hints\n- `New` – Inline Toolbar has new look 💅\n- `New` – Inline Tool's `render()` now supports [Menu Config](https://editorjs.io/menu-config/) format\n- `New` – *ToolsAPI* – All installed block tools now accessible via ToolsAPI `getBlockTools()` method\n- `New` – *SelectionAPI* – Exposed methods `save()` and `restore()` that allow to save selection to be able to temporally move focus away, methods `setFakeBackground()` and `removeFakeBackground()` that allow to immitate selection while focus moved away\n- `New` – *BlocksAPI* – Exposed `getBlockByElement()` method that helps find block by any child html element\n- `New` – \"Convert to\" control is now also available in Block Tunes\n- `New` — Editor.js now supports contenteditable placeholders out of the box. Just add `data-placeholder` or `data-placeholder-active` attribute to make it work. The first one will work like native placeholder while the second one will show placeholder only when block is current.\n- `Improvement` — Now Paragraph placeholder will be shown for the current paragraph, not only the first one.\n- `Improvement` - The API `blocks.update` now accepts `tunes` data as optional third argument and makes `data` - block data as optional.\n- `Improvement` — The ability to merge blocks of different types (if both tools provide the conversionConfig)\n- `Improvement` - The API `blocks.convert()` now returns the new block API\n- `Improvement` - The API `caret.setToBlock()` now can accept either BlockAPI or block index or block id\n- `Improvement` – *MenuConfig* – `TunesMenuConfig` type is deprecated, use the `MenuConfig` instead\n– `Improvement` — *Types* — `BlockToolConstructorOptions` type improved, `block` and `config` are not optional anymore\n- `Improvement` - The Plus button and Block Tunes toggler are now better aligned with large line-height blocks, such as Headings\n- `Improvement` — Creating links on Android devices: now the mobile keyboard will have an \"Enter\" key for accepting the inserted link.\n- `Improvement` — Placeholders will stay visible on inputs focus.\n– `Refactoring` – Switched to Vite as Cypress bundler\n- `Fix` — `onChange` will be called when removing the entire text within a descendant element of a block.\n- `Fix` - Unexpected new line on Enter press with selected block without caret\n- `Fix` - Search input autofocus loosing after Block Tunes opening\n- `Fix` - Block removing while Enter press on Block Tunes\n- `Fix` – Unwanted scroll on first typing on iOS devices\n- `Fix` - Unwanted soft line break on Enter press after period and space (\". |\") on iOS devices\n- `Fix` - Caret lost after block conversion on mobile devices.\n- `Fix` - Caret lost after Backspace at the start of block when previoius block is not convertable\n– `Fix` — Deleting whitespaces at the start/end of the block\n- `Fix` — The problem caused by missed \"import type\" in block mutation event types resolved\n\n### 2.29.1\n\n- `Fix` — Toolbox wont be shown when Slash pressed with along with Shift or Alt\n- `Fix` — Toolbox will be opened when Slash pressed in non-US keyboard layout where there is no physical '/' key.\n\n### 2.29.0\n\n- `New` — Editor Config now has the `style.nonce` attribute that could be used to allowlist editor style tag for Content Security Policy \"style-src\"\n- `New` — Toolbox now will be opened by '/' in empty Block instead of Tab\n- `New` — Block Tunes now will be opened by 'CMD+/' instead of Tab in non-empty block\n- `New` — Tab now will navigate through Blocks. In last block Tab will navigate to the next input on page.\n- `Fix` — Passing an empty array via initial data or `blocks.render()` won't break the editor\n- `Fix` — Layout did not shrink when a large document cleared in Chrome\n- `Fix` — Multiple Tooltip elements creation fixed\n- `Fix` — When the focusing Block is out of the viewport, the page will be scrolled.\n- `Fix` - Compiler error \"This import is never used as a value and must use 'import type'...\" fixed\n- `Fix` — `blocks.render()` won't lead the `onChange` call in Safari\n- `Fix` — Editor wrapper element growing on the Inline Toolbar close\n- `Fix` — Fix errors thrown by clicks on a document when the editor is being initialized\n- `Fix` — Caret losing on Mobile Devices when adding a block via Toolbox or via Backspace at the beginning of a Block\n- `Improvement` — Now you can set focus via arrows/Tab to \"contentless\" (decorative) blocks like Delimiter which have no inputs.\n- `Improvement` — Inline Toolbar sometimes opened in an incorrect position. Now it will be aligned by the left side of the selected text. And won't overflow the right side of the text column.\n- `Improvement` - Now the `data-mutation-free` supports deep nesting, so you can mark some element with it to prevent the onChange call caused by child element mutating\n- `Improvement` - Now the `data-mutation-free` also allows to skip \"characterData\" mutations (eg. text content change)\n- `Refactoring` — `ce-block--focused` class toggling removed as unused.\n\n### 2.28.2\n\n- `Fix` — Get rid of redundant logs from the build\n\n### 2.28.1\n\n- `Fix` — Some Block were be skipped on saving after pasting them as HTML\n\n### 2.28.0\n\n- `New` - Block ids now displayed in DOM via a data-id attribute. Could be useful for plugins that want to access a Block's element by id.\n- `New` - The `blocks.convert(blockId, newType)` API method was added. It allows to convert existing Block to a Block of another type.\n- `New` - The `blocks.insertMany()` API method added. It allows to insert several Blocks to the specified index.\n- `Improvement` - The Delete keydown at the end of the Block will now work opposite a Backspace at the start. Next Block will be removed (if empty) or merged with the current one.\n- `Improvement` - The Delete keydown will work like a Backspace when several Blocks are selected.\n- `Improvement` - If we have two empty Blocks, and press Backspace at the start of the second one, the previous will be removed instead of the current.\n- `Improvement` - Tools shortcuts could be used to convert one Block to another.\n- `Improvement` - Tools shortcuts displayed in the Conversion Toolbar\n- `Improvement` - Initialization Loader has been removed.\n- `Improvement` - Selection style won't override your custom style for `::selection` outside the editor.\n- `Improvement` - Performance optimizations: initialization speed increased, `blocks.render()` API method optimized. Big documents will be displayed faster.\n- `Improvement` - \"Editor saving\" log removed\n- `Improvement` - \"I'm ready\" log removed\n- `Improvement` - The stub-block style is simplified.\n- `Improvement` - If some Block's tool throws an error during construction, we will show Stub block instead of skipping it during render\n- `Improvement` - Call of `blocks.clear()` now will trigger onChange with \"block-removed\" event for all removed blocks.\n- `Improvement` - The `blocks.clear()` now can be awaited.\n- `Improvement` - `BlockMutationType` and `BlockMutationEvent` types exported\n- `Improvement` - `blocks.update(id, data)` now can accept partial data object — it will update only passed properties, others will remain the same.\n- `Improvement` - `blocks.update(id, data)` now will trigger onChange with only `block-change` event.\n- `Improvement` - `blocks.update(id, data)` will return a promise with BlockAPI object of the changed block.\n\n\n### 2.27.2\n\n- `Fix` - `onChange` won't be called when element with data-mutation-free changes some attribute\n\n### 2.27.1\n\n- `Fix` - `onChange` will be called on removing the whole text in a block\n\n### 2.27.0\n\n- `New` — *Toolbar API* — Added a new method for toggling the toolbox.\n- `New` — Added types for block mutation events\n- `New` — Batching added to the `onChange` callback. Now the second argument can contain an array of CustomEvents as well as a single one. Multiple changes made in a short period of time will be batched under a single `onChange` call.\n- `Improvement` — *Toolbox* — Number of `close()` method calls optimized.\n- `Improvement` — The `onChange` callback can be muted if all mutations contain nodes with the `data-mutation-free` attribute.\n- `Improvement` — Pressing \"Enter\" at the end of a Block won't lead to redundant `block-changed` event triggering. Only `block-added` event will be dispatched.\n- `Improvement` — The block mutation handler is now called on every block change (including background changes), instead of only when a block is focused\n- `Improvement` — Number of caret saving method calls optimized for Block Tunes opening/closing.\n- `Improvement` — Package size reduced by removing redundant files.\n- `Refactoring` — Switched from Webpack to Vite as the build system.\n- `Refactoring` — *Dependencies* — Upgraded Cypress to v12 and related libraries to the latest versions.\n- `Refactoring` — *Dependencies* — Upgraded TypeScript to v5.\n- `Refactoring` — `EventDispatcher` types improved. Now we can pass `EventsMap` via generic to specify a map of event names and their payloads that can be used in a particular EventDispatcher instance.\n- `Refactoring` — All events in common editor Event Bus now have own type declarations.\n- `Refactoring` — Removed the block mutation observer from blocks and attached a single observer to the editor's blocks wrapper element.\n- `Refactoring` — Removed the debounce from the block mutation handler and used batching instead.\n- `Refactoring` — Refactored the popover class for better performance and maintenance.\n- `Fix` — The `onChange` callback won't trigger when block tunes are opened or closed.\n- `Fix` — Resolved a compiler error caused by importing the `BlockToolData` type.\n- `Fix` — Resolved a problem where the document would scroll to the beginning after moving a block above the viewport.\n- `Fix`- Fixed several bugs caused by browser extensions — Removed the search for a block's container in the DOM on saving and kept it in memory instead, updating it when the tool changes a container element.\n- `Fix` — *ToolsAPI* — `pasteConfig` getter with `false` value could be used to disable paste handling by Editor.js core. Could be useful if your tool has its own paste handler.\n- `CI` — Ubuntu container is now used for Edge tests runner.\n- `CI` — Node 16 is used for GitHib Actions.\n\n### 2.26.5\n\n- `Fix` — *Types* — Remove unnecessary import that creates a dependency on the `cypress`.\n\n### 2.26.4\n\n- `Improvement` — *Menu Config* — Property `label` renamed to `title`.\n\n### 2.26.3\n\n- `Fix` — *Paste Module* — fix for a problem with specifying of `pasteConfig().tags` in upper case  [#2208](https://github.com/codex-team/editor.js/issues/2208).\n\n### 2.26.2\n\n- `Fix` — *Menu Config* — Installed tunes are rendered above default tunes again.\n\n### 2.26.1\n\n- `Improvement` — *Menu Config* — Now it becomes possible to create toggle groups.\n\n### 2.26.0\n\n- `New` — *UI* — Block Tunes became vertical just like the Toolbox 🤩\n- `New` — *Block Tunes API* — Now `render()` method of a Block Tune can return config with just icon, label and callback instead of custom HTML. This improvement is a key to the new straightforward way of configuring tune's appearance in Block Tunes menu.\n- `New` — *Tools API* — As well as `render()` in `Tunes API`, Tool's `renderSettings()` now also supports new configuration format.\n- `New` — *UI* — Meet the new icons from [CodeX Icons](https://github.com/codex-team/icons) pack 🛍 💝\n- `New` — *BlocksAPI* — the `blocks.insert()` method now also have the optional `id` param. If passed, this id will be used instead of the generated one.\n- `Deprecated` — *Styles API* — CSS classes `.cdx-settings-button` and `.cdx-settings-button--active` are not recommended to use. Consider configuring your block settings with new JSON API instead.\n- `Fix` — Wrong element not highlighted anymore when popover opened.\n- `Fix` — When Tunes Menu open keydown events can not be handled inside plugins.\n- `Fix` — If a Tool specifies some tags to substitute on paste, all attributes of that tags will be removed before passing them to the tool. Possible XSS vulnerability fixed.\n- `Fix` — Pasting from Microsoft Word to Chrome (Mac OS) fixed. Now if there are no image-tools connected, regular text content will be pasted.\n- `Fix` — Workaround for the HTMLJanitor bug with Tables (https://github.com/guardian/html-janitor/issues/3) added\n- `Fix` — Toolbox shortcuts appearance and execution fixed [#2112](https://github.com/codex-team/editor.js/issues/2112)\n- `Fix` — Inline Tools click handling on mobile devices improved\n- `Improvement` — *Tools API* — `pasteConfig().tags` now support sanitizing configuration. It allows you to leave some explicitly specified attributes for pasted content.\n- `Improvement` — *CodeStyle* — [CodeX ESLint Config](https://github.com/codex-team/eslint-config) has bee updated. All ESLint/Spelling issues resolved\n- `Improvement` — *ToolsAPI* — The `icon` property of the `toolbox` getter became optional.\n\n\n### 2.25.0\n\n- `New` — *Tools API* — Introducing new feature — toolbox now can have multiple entries for one tool! <br>\nDue to that API changes: tool's `toolbox` getter now can return either a single config item or an array of config items\n- `New` — *Blocks API* — `composeBlockData()` method was added.\n\n### 2.24.4\n\n- `Fix` — Keyboard selection by word [#2045](https://github.com/codex-team/editor.js/issues/2045)\n\n### 2.24.3\n\n- `Fix` — Issue with toolbox preventing text selection fixed\n\n### 2.24.2\n\n- `Fix` — Scrolling issue when opening toolbox on mobile fixed\n- `Fix` — Typo in toolbox empty placeholder fixed\n- `Fix` — The issue with scroll jumping on block hovering have fixed [2036](https://github.com/codex-team/editor.js/issues/2036)\n- `Improvement` — *Dev Example Page* - Add popup example page\n- `Improvement` — *UI* - The Toolbox will restore the internal scroll on every opening\n\n### 2.24.1\n\n— `Fix` — The I18n of Tools` titles at the Toolbox now works correctly [#2030](https://github.com/codex-team/editor.js/issues/2030)\n\n### 2.24.0\n\n- `New` — *UI* — The Toolbox became vertical 🥳\n- `Improvement` — *UI* — the Plus button will always be shown (previously, it appears only for empty blocks)\n- `Improvement` — *Dev Example Page* - Server added to allow opening example page on other devices in network.\n- `Fix` — `UI` — the Toolbar won't move on hover at mobile viewports. Resolves [#1972](https://github.com/codex-team/editor.js/issues/1972)\n- `Fix` — `OnChange` event invocation after block insertion. [#1997](https://github.com/codex-team/editor.js/issues/1997)\n- `Fix` — `ReadOnly` — the `readonly.isEnabled` API getter now works correctly after `readonly.toggle()` calling. Resolves [#1822](https://github.com/codex-team/editor.js/issues/1822)\n- `Fix` — `Paste` — the inline HTML tags now will be preserved on pasting. [#1686](https://github.com/codex-team/editor.js/pull/1686)\n\n### 2.23.2\n\n— `Fix` — Crash on initialization in the read-only mode [#1968](https://github.com/codex-team/editor.js/issues/1968)\n\n### 2.23.1\n\n— `Fix` — Incorrect release tag fixed\n\n### 2.23.0\n\n- `Improvement` — *EditorConfig* — The `onChange` callback now accepts two arguments: EditorJS API and the CustomEvent with `type` and `detail` allowing to determine what happened with a Block\n- `New` — *Block API* — The new `dispatchChange()` method allows to manually trigger the 'onChange' callback. Useful when Tool made a state mutation that is invisible for editor core.\n- `Improvement` — *UI* — Block Tunes toggler moved to the left\n- `Improvement` — *UI* — Block Actions (BT toggler + Plus Button) will appear on block hovering instead of click\n- `Improvement` — *UI* — Block Tunes toggler icon and Plus button icon updated\n- `Improvement` — *Dev Example Page* — The menu with helpful buttons added to the bottom of the screen\n- `Improvement` — *Dev Example Page* — The 'dark' theme added. Now we can code at night more comfortably.\n- `Improvement` — *Rectangle Selection* — paint optimized\n- `Fix` — *Rectangle Selection* — the first click after RS was not clear selection state. Now does.\n- `Improvement` — *Blocks API* — toolbar moving logic removed from `blocks.move()` and `blocks.swap()` methods. Instead, you should use Toolbar API (it was used by MoveUp and MoveDown tunes, they were updated).\n- `New` — *Blocks API* — The `getBlockIndex()` method added\n- `New` — *Blocks API* — the `insert()` method now has the `replace: boolean` parameter\n- `New` — *Blocks API* —  the `insert()` method now returns the inserted `Block API`\n- `New` — *Listeners API* — the `on()` method now returns the listener id.\n- `New` — *Listeners API* — the new `offById()` method added\n- `New` — `API` — The new `UiApi` section was added. It allows accessing some editor UI nodes and methods.\n- `Refactoring` — Toolbox became a standalone class instead of a Module. It can be accessed only through the Toolbar module.\n- `Refactoring` — CI flow optimized.\n- `Fix` - Recognize async `onPaste` handlers in tools [#1803](https://github.com/codex-team/editor.js/issues/1803).\n- `Fix` — Fire onChange event for native inputs [#1750](https://github.com/codex-team/editor.js/issues/1750)\n\n### 2.22.3\n\n- `Fix` — Tool config is passed to `prepare` method [editor-js/embed#68](https://github.com/editor-js/embed/issues/68)\n\n### 2.22.2\n\n- `Improvement` — Inline Toolbar might be used for any contenteditable element inside Editor.js zone\n- `Improvement` *Tunes API* - Tunes now can provide sanitize configuration\n- `Fix` *Tunes API* - Tune config now passed to constructor under `config` property\n- `Fix` *Types* - Add common type for internal and external Tools configuration\n- `Fix` — Block's destroy method is called on block deletion\n- `Fix` - Fix jump to the button of editor zone on CBS\n\n### 2.22.1\n\n- `Fix` — I18n for internal Block Tunes [#1661](https://github.com/codex-team/editor.js/issues/1661)\n\n### 2.22.0\n\n- `New` - `onChange` callback now receive Block API object of affected block\n- `New` - API method `blocks.update(id, data)` added.\n\n### 2.21.0\n\n- `New` - Blocks now have unique ids [#873](https://github.com/codex-team/editor.js/issues/873)\n\n### 2.20.2\n\n- `Fix` — Append default Tunes if user tunes are provided for Block Tool [#1640](https://github.com/codex-team/editor.js/issues/1640)\n- `Fix` - Prevent the leak of codex-tooltip when Editor.js is destroyed [#1475](https://github.com/codex-team/editor.js/issues/1475).\n- `Refactoring` - Notifier module now is a util.\n\n### 2.20.1\n\n- `Fix` - Create a new block when clicked at the bottom [#1588](https://github.com/codex-team/editor.js/issues/1588).\n- `Fix` — Fix sanitization problem with Inline Tools [#1631](https://github.com/codex-team/editor.js/issues/1631)\n- `Fix` — Fix copy in FireFox [1625](https://github.com/codex-team/editor.js/issues/1625)\n- `Refactoring` - The Sanitizer module is util now.\n- `Refactoring` - Tooltip module is util now.\n- `Refactoring` — Refactoring based on LGTM [#1577](https://github.com/codex-team/editor.js/issues/1577).\n- `Refactoring` — Refactoring based on ESLint [#1636](https://github.com/codex-team/editor.js/issues/1636).\n\n### 2.20.0\n\n- `New` — [Block Tunes API](block-tunes.md) added\n\n### 2.19.3\n\n- `Fix` — Ignore error raised by Shortcut module\n\n### 2.19.2\n\n- `New` - `toolbar.toggleBlockSettings()` API method added [#1442](https://github.com/codex-team/editor.js/issues/1421).\n- `Improvements` - A generic type for Tool config added [#1516](https://github.com/codex-team/editor.js/issues/1516)\n- `Improvements` - Remove unused `force` option in `Caret.navigateNext()` and `Caret.navigatePrevious()` [#857](https://github.com/codex-team/editor.js/issues/857#issuecomment-770363438).\n- `Improvements` - Remove bundles from the repo [#1541](https://github.com/codex-team/editor.js/pull/1541).\n- `Improvements` - Document will be scrolled when blocks are selected with `SHIFT+UP` or `SHIFT+DOWN` [#1447](https://github.com/codex-team/editor.js/issues/1447)\n- `Improvements` - The caret will be set on editor copy/paste [#1470](https://github.com/codex-team/editor.js/pull/1470)\n- `Improvements` - Added generic types to OutputBlockData [#1551](https://github.com/codex-team/editor.js/issues/1551).\n- `Fix` - Fix BlockManager.setCurrentBlockByChildNode() with multiple Editor.js instances [#1503](https://github.com/codex-team/editor.js/issues/1503).\n- `Fix` - Fix an unstable block cut process [#1489](https://github.com/codex-team/editor.js/issues/1489).\n- `Fix` - Type definition of the Sanitizer config: the sanitize function now contains param definition [#1491](https://github.com/codex-team/editor.js/pull/1491).\n- `Fix` - Fix unexpected behavior on an empty link pasting [#1348](https://github.com/codex-team/editor.js/issues/1348).\n- `Fix` - Fix SanitizerConfig type definition [#1513](https://github.com/codex-team/editor.js/issues/1513)\n- `Refactoring` - The Listeners module now is a util.\n- `Refactoring` - The Events module now is a util.\n- `Fix` - Editor Config now immutable [#1552](https://github.com/codex-team/editor.js/issues/1552).\n- `Refactoring` - Shortcuts module is util now.\n- `Fix` - Fix bubbling on BlockManagers' listener [#1433](https://github.com/codex-team/editor.js/issues/1433).\n\n\n### 2.19.1\n\n- `Improvements` - The [Cypress](https://www.cypress.io) was integrated as the end-to-end testing framework\n- `Improvements` - Native `typeof`replaced with custom utils methods\n- `Improvements` - Bind shortcuts listeners on the editor wrapper instead of document [#1391](https://github.com/codex-team/editor.js/issues/1391)\n- `Fix` - The problem with destroy() method [#1380](https://github.com/codex-team/editor.js/issues/1380).\n- `Fix` - add getter keyword to `block.mergeable` method [#1415](https://github.com/codex-team/editor.js/issues/1415).\n- `Fix` — Fix problem with entering to Editor.js by Tab key [#1393](https://github.com/codex-team/editor.js/issues/1393)\n- `Fix` - Sanitize pasted block data [#1396](https://github.com/codex-team/editor.js/issues/1396).\n- `Fix` - Unnecessary block creation after arrow navigation at last non-default block[#1414](https://github.com/codex-team/editor.js/issues/1414)\n\n### 2.19\n\n- `New` - Read-only mode 🥳 [#837](https://github.com/codex-team/editor.js/issues/837)\n- `New` - RTL mode added [#670](https://github.com/codex-team/editor.js/issues/670)\n- `New` - Allows users to provide common `inlineToolbar` property which will be used for all tools whose `inlineToolbar` property is set to `true`. It can be overridden by the tool's own `inlineToolbar` property. Also, inline tools will be ordered according to the order of the inline tools in array provided in the `inlineToolbar` property. [#1056](https://github.com/codex-team/editor.js/issues/1056)\n- `New` - Tool's `reset` static method added to the API to clean up any data added by Tool on initialization\n- `Improvements` - The `initialBlock` property of Editor config is deprecated. Use the `defaultBlock` instead. [#993](https://github.com/codex-team/editor.js/issues/993)\n- `Improvements` - BlockAPI `call()` method now returns the result of calling method, thus allowing it to expose arbitrary data as needed [#1205](https://github.com/codex-team/editor.js/pull/1205)\n- `Improvements` - Useless log about missed i18n section has been removed  [#1269](https://github.com/codex-team/editor.js/issues/1269)\n- `Improvements` - Allowed to set `false` as `toolbox` config in order to hide Toolbox button [#1221](https://github.com/codex-team/editor.js/issues/1221)\n- `Fix` — Fix problem with types usage [#1183](https://github.com/codex-team/editor.js/issues/1183)\n- `Fix` - Fixed issue with Spam clicking the \"Click to tune\" button duplicates the icons on FireFox. [#1273](https://github.com/codex-team/editor.js/issues/1273)\n- `Fix` - Fixed issue with `editor.blocks.delete(index)` method which throws an error when Editor.js is not focused, even after providing a valid index. [#1182](https://github.com/codex-team/editor.js/issues/1182)\n- `Fix` - Fixed the issue of toolbar not disappearing on entering input in Chinese, Hindi and some other languages. [#1196](https://github.com/codex-team/editor.js/issues/1196)\n- `Fix` - Do not stop events propagation if not needed (essential for React synthetic events) [#1051](https://github.com/codex-team/editor.js/issues/1051) [#946](https://github.com/codex-team/editor.js/issues/946)\n- `Fix` - Tool's `destroy` method is not invoked when `editor.destroy()` is called. [#1047](https://github.com/codex-team/editor.js/issues/1047)\n- `Fix` - Fixed issue with enter key in inputs and textareas [#920](https://github.com/codex-team/editor.js/issues/920)\n- `Fix` - blocks.getBlockByIndex() API method now returns void for indexes out of range [#1270](https://github.com/codex-team/editor.js/issues/1270)\n- `Fix` - Fixed the `Tab` key behavior when the caret is not set inside contenteditable element, but the block is selected [#1302](https://github.com/codex-team/editor.js/issues/1302).\n- `Fix` - Fixed the `onChange` callback issue. This method didn't be called for native inputs before some contenteditable element changed [#843](https://github.com/codex-team/editor.js/issues/843)\n- `Fix` - Fixed the `onChange` callback issue. This method didn't be called after the callback throws an exception [#1339](https://github.com/codex-team/editor.js/issues/1339)\n- `Fix` - The internal `shortcut` getter of Tools classes will work now.\n- `Deprecated` — The Inline Tool `clear()` method is deprecated because the new instance of Inline Tools will be created on every showing of the Inline Toolbar\n\n### 2.18\n\n- `New` *I18n API* — Ability to provide internalization for Editor.js core and tools. [#751](https://github.com/codex-team/editor.js/issues/751)\n- `New` — Block API that allows you to access certain Block properties and methods\n- `Improvements` - TSLint (deprecated) replaced with ESLint, old config changed to [CodeX ESLint Config](https://github.com/codex-team/eslint-config).\n- `Improvements` - Fix many code-style issues, add missed annotations.\n- `Improvements` - Adjusted GitHub action for ESLint.\n- `Improvements` - Blocks API: if `blocks.delete` method is called, but no Block is selected, show warning instead of throwing an error [#1102](https://github.com/codex-team/editor.js/issues/1102)\n- `Improvements` - Blocks API: allow deletion of blocks by specifying block index via `blocks.delete(index)`.\n- `Improvements` - UX: Navigate next Block from the last non-initial one creates new initial Block now [#1103](https://github.com/codex-team/editor.js/issues/1103)\n- `Improvements` - Improve performance of DOM traversing at the `isEmpty()` method [#1095](https://github.com/codex-team/editor.js/issues/1095)\n- `Improvements` - CODE OF CONDUCT added\n- `Improvements` - Disabled useCapture flag for a block keydown handling. That will allow plugins to override keydown and stop event propagation, for example, to make own Tab behavior.\n- `Improvements` - All modules now might have `destroy` method called on Editor.js destroy\n- `Improvements` - Block settings can contain text inputs, focus will be restored after settings closed [#1090](https://github.com/codex-team/editor.js/issues/1090)\n- `Fix` - Editor's styles won't be appended to the `<head>` when another instance have already do that [#1079](https://github.com/codex-team/editor.js/issues/1079)\n- `Fix` - Fixed wrong toolbar icon centering in Firefox [#1120](https://github.com/codex-team/editor.js/pull/1120)\n- `Fix` - Toolbox: Tool's order in Toolbox now saved in accordance with `tools` object keys order [#1073](https://github.com/codex-team/editor.js/issues/1073)\n- `Fix` - Setting `autofocus` config property to `true` cause adding `.ce-block--focused` for the autofocused block  [#1073](https://github.com/codex-team/editor.js/issues/1124)\n- `Fix` - Public getter `shortcut` now works for Inline Tools [#1132](https://github.com/codex-team/editor.js/issues/1132)\n- `Fix` - `CMD+A` handler removed after Editor.js destroy [#1133](https://github.com/codex-team/editor.js/issues/1133)\n\n>  *Breaking changes* `blocks.getBlockByIndex` method now returns BlockAPI object. To access old value, use BlockAPI.holder property\n\n### 2.17\n\n- `Improvements` - Editor's [onchange callback](https://editorjs.io/configuration#editor-modifications-callback) now accepts an API as a parameter\n- `Fix` - Some mistakes are fixed in [installation.md](installation.md)\n- `Fix` - Fixed multiple paste callback triggering in a case when several editors are instantiated [#1011](https://github.com/codex-team/editor.js/issues/1011)\n- `Fix` - Fixed inline toolbar flipper activation on closing conversion toolbar [#995](https://github.com/codex-team/editor.js/issues/995)\n- `Improvements` - New window tab is opened by clicking on anchor with ctrl [#1057](https://github.com/codex-team/editor.js/issues/1057)\n- `Fix` - Fix block-tune buttons alignment in some CSS-resetors that forces `box-sizing: border-box` rule [#1003](https://github.com/codex-team/editor.js/issues/1003)\n- `Improvements` - New style of a Block Settings button. Focused block background removed.\n- `New` — Add in-house copy-paste support through `application/x-editor-js` mime-type\n- `New` Block [lifecycle hook](tools.md#block-lifecycle-hooks) `moved`\n- `Deprecated` — [`blocks.swap(fromIndex, toIndex)`](api.md) method is deprecated. Use `blocks.move(toIndex, fromIndex)` instead.\n- `Fix` — Improve plain text paste [#1012](https://github.com/codex-team/editor.js/issues/1012)\n- `Fix` — Fix multiline paste [#1015](https://github.com/codex-team/editor.js/issues/1015)\n\n\n### 2.16.1\n\n- `Fix` — Fix Firefox bug with incorrect height and cursor position of empty content editable elements [#947](https://github.com/codex-team/editor.js/issues/947) [#876](https://github.com/codex-team/editor.js/issues/876) [#608](https://github.com/codex-team/editor.js/issues/608) [#876](https://github.com/codex-team/editor.js/issues/876)\n- `Fix` — Set initial hidden Inline Toolbar position [#979](https://github.com/codex-team/editor.js/issues/979)\n- `Fix` — Fix issue with CodeX.Tooltips TypeScript definitions [#978](https://github.com/codex-team/editor.js/issues/978)\n- `Fix` — Fix some issues with Inline and Tunes toolbars.\n- `Fix` - Fix `minHeight` option with zero-value issue [#724](https://github.com/codex-team/editor.js/issues/724)\n- `Improvements` — Disable Conversion Toolbar if there are no Tools to convert [#984](https://github.com/codex-team/editor.js/issues/984)\n\n### 2.16\n\n- `Improvements` — Inline Toolbar design improved\n- `Improvements` — Conversion Toolbar now included in the Inline Toolbar [#853](https://github.com/codex-team/editor.js/issues/853)\n- `Improvements` — All buttons now have beautiful Tooltips provided by [CodeX Tooltips](https://github.com/codex-team/codex.tooltips)\n- `New` — new Tooltips API for displaying tooltips near your custom elements\n- `New` *API* — Block [lifecycle hooks](tools.md#block-lifecycle-hooks)\n- `New` *Inline Tools API* — Ability to specify Tool's title via `title` static getter.\n- `Fix` — On selection from end to start backspace is working as expected now [#869](https://github.com/codex-team/editor.js/issues/869)\n- `Fix` — Fix flipper with empty dom iterator [#926](https://github.com/codex-team/editor.js/issues/926)\n- `Fix` — Normalize node before walking through children at `isEmpty` method [#943](https://github.com/codex-team/editor.js/issues/943)\n- `Fix` — Fixed Grammarly conflict [#779](https://github.com/codex-team/editor.js/issues/779)\n- `Improvements` — Module Listeners now correctly removes events with options [#904](https://github.com/codex-team/editor.js/pull/904)\n- `Improvements` — Styles API: `.cdx-block` default vertical margins decreased from 0.7 to 0.4 ems.\n- `Fix` — Fixed `getRangeCount` call if range count is 0 [#938](https://github.com/codex-team/editor.js/issues/938)\n- `New` — Log levels now available to suppress Editor.js console messages [#962](https://github.com/codex-team/editor.js/issues/962)\n- `Fix` — Fixed wrong navigation on block deletion\n\n### 2.15.1\n\n- `Refactoring` — Constants of tools settings separated by internal and external to correspond API\n- `Refactoring` — Created universal Flipper class that responses for navigation by keyboard inside of any Toolbars\n- `Fix` — First CMD+A on block with now uses default behaviour. Fixed problem with second CMD+A after selection clearing [#827](https://github.com/codex-team/editor.js/issues/827)\n- `Improvements` — Style of inline selection and selected blocks improved\n- `Fix` - Fixed problem when property 'observer' in modificationObserver is not defined\n\n### 2.15\n\n- `New` — New [`blocks.insert()`](api.md) API method [#715](https://github.com/codex-team/editor.js/issues/715).\n- `New` *Conversion Toolbar* — Ability to convert one block to another [#704](https://github.com/codex-team/editor.js/issues/704)\n- `New` *Cross-block selection* — Ability to select multiple blocks by mouse and with SHIFT+ARROWS [#703](https://github.com/codex-team/editor.js/issues/703)\n- `Deprecated` — [`blocks.insertNewBlock()`](api.md) method is deprecated. Use `blocks.insert()` instead.\n- `Improvements` — Inline Toolbar now works on mobile devices [#706](https://github.com/codex-team/editor.js/issues/706)\n- `Improvements` — Toolbar looks better on mobile devices [#706](https://github.com/codex-team/editor.js/issues/706)\n- `Improvements` — Now `pasteConfig` can return `false` to disable paste handling on your Tool [#801](https://github.com/codex-team/editor.js/issues/801)\n- `Fix` — EditorConfig's `onChange` callback now fires when native inputs\\` content has been changed [#794](https://github.com/codex-team/editor.js/issues/794)\n- `Fix` — Resolve bug with deleting leading new lines [#726](https://github.com/codex-team/editor.js/issues/726)\n- `Fix` — Fix inline link Tool to support different link types like `mailto` and `tel` [#809](https://github.com/codex-team/editor.js/issues/809)\n- `Fix` — Added `typeof` util method to check exact object type [#805](https://github.com/codex-team/editor.js/issues/805)\n- `Fix` — Remove internal `enableLineBreaks` option from external Tool settings type description [#825](https://github.com/codex-team/editor.js/pull/825)\n\n### 2.14\n\n- `Fix` *Config* — User config now has higher priority than internal settings [#771](https://github.com/codex-team/editor.js/issues/771)\n- `New` — Ability to work with Block Actions and Inline Toolbar from the keyboard by Tab. [#705](https://github.com/codex-team/editor.js/issues/705)\n- `Fix` — Fix error thrown by click on the empty editor after `blocks.clear()` method calling [#761](https://github.com/codex-team/editor.js/issues/761)\n- `Fix` — Fix placeholder property appearance. Now you can assign it via `placeholder` property of EditorConfig. [#714](https://github.com/codex-team/editor.js/issues/714)\n- `Fix` — Add API shorthands to TS types [#788](https://github.com/codex-team/editor.js/issues/788)\n\n### 2.13\n\n- `Improvements` *BlockSelection* — Block Selection allows to select single editable element via CMD+A\n- `New` *API* — Added [API methods](api.md) to open and close inline toolbar [#665](https://github.com/codex-team/editor.js/issues/665)\n- `New` *Config* - Added new property in EditorConfig `holder`, use this property for append Editor instead `holderId`. `holder` property now support reference on dom element. [#696](https://github.com/codex-team/editor.js/issues/696)\n- `Deprecated` *Config* - `holderId` property now is deprecated and will removed in next major release. Use `holder` instead.\n- `Fix` *Types* — Fixed error with `codex-notifier` package [#713](https://github.com/codex-team/editor.js/issues/713)\n- `Improvements` — Close inline toolbar after creating a new link.\n- `New` *Config* — Option `minHeight` for customizing Editor's bottom zone height added.\n\n### 2.12.4\n\n- `Improvements` — CodeX.Shortcuts version updated to the v1.1 [#684](https://github.com/codex-team/editor.js/issues/684)\n- `Fix` — Do not start multi-block selection on Toolbox and Inline Toolbar [#646](https://github.com/codex-team/editor.js/issues/646)\n- `Fix` — Minor fixes of caret behaviour [#663](https://github.com/codex-team/editor.js/issues/663)\n- `Fix` — Fix inline-link icon position in Firefox [#674](https://github.com/codex-team/editor.js/issues/674)\n\n### 2.12.3\n\n- `Fix` — Make Toolbox tooltip position font-size independent\n\n### 2.12.2\n\n- New *Inline Tools* — pass tool settings from configuration to Tool constructor\n\n### 2.12.1\n\n- `Fix` — Fix processing `color-mod` function in styles\n\n### 2.12.0\n\n- `New` *API* - new `blocks` API method `renderFromHTML`\n\n### 2.11.11\n\n- `New` — Add ability to pass configuration for internal Tools\n\n### 2.11.10\n\n- `Fix` - Fix editor view on mobile devices\n\n### 2.11.9\n\n- `Fix` - Fix inline toolbar buttons margin. Update dependencies list. Update tools for example page.\n\n### 2.11.8\n\n- `Fix` — Block tunes margins now better works with more than 3 buttons\n\n### 2.11.7\n\n- `Fix` *Paste* — Fix pasting into non-initial Blocks\n\n### 2.11.6\n\n- `Fix` *Paste* — Polyfill for Microsoft Edge\n\n### 2.11.5\n\n- `Fix` *RectangleSelection* — Redesign of the scrolling zones\n\n### 2.11.4\n\n- `Fix` - Clear focus when click is outside the Editor instance\n\n### 2.11.3\n\n- `Fix` — Fix CMD+A Selection on multiple Editor instances\n\n### 2.11.2\n\n- `Improvements` — Docs updated and common enhancements\n\n### 2.11.1\n\n- `Fix` *RectangleSelection* — Selection is available only for the main mouse button\n\n### 2.11.0\n\n- `New` — Add API methods shorthands\n\n### 2.10.0\n\n- `New` — Rename from CodeX Editor to Editor.js\n\n### 2.9.5\n\n- `New` — Toolbox now have beautiful helpers with Tool names and shortcuts\n\n### 2.9.4\n\n- `Improvements` — Prevent navigating back on Firefox when Block is removing by backspace\n\n### 2.9.3\n\n- `Fix` — Handle paste only on initial Block\n\n### 2.9.2\n\n- `New` — Blocks selected with Rectangle Selection can be also removed, copied or cut\n\n### 2.9.1\n\n- `Improvements` — Migrate from `postcss-cssnext` to `postcss-preset-env` and disable `postcss-custom-properties` which conflicts with `postcss-preset-env`\n\n### 2.9.0\n\n- `New` *RectangleSelection* — Ability to select Block or several Blocks with mouse\n\n### 2.8.1\n\n- `Fix` *Caret* — Fix \"History back\" call on backspace in Firefox\n\n### 2.8.0\n\n- `Improvements` *API* — Added [API methods](api.md#caretapi) to manage caret position\n\n### 2.7.32\n\n- `Improvements` *Types* — TypeScript types sre updated\n\n### 2.7.31\n\n- `Fix` — Caret now goes through <input> elements without `type` attribute\n\n### 2.7.30\n\n- `Fix` — Fixed selection behavior when text has modifiers form Inline Toolbar\n\n### 2.7.29\n\n- `Fix` — cmd+x works only for custom selection now\n\n### 2.7.28\n\n- `New` [Tools Validation](https://github.com/codex-team/editor.js/blob/master/docs/tools.md#validate-optional) is added.\n\n### 2.2.27\n\n- `New` *Mobile view* — Editor now adopted for mobile devices\n- `New` *Narrow mode* — Editor now adopted for narrow containers\n\n### 2.2.26\n\n- `Improvements` *Caret* — Improvements of the caret behaviour: arrows, backspace and enter keys better handling.\n\n### 2.2.25\n\n- `New` *Autofocus* — Now you can set focus at Editor after page has been loaded\n\n### 2.2.24\n\n- `Improvements` *Paste* handling — minor paste handling improvements\n\n### 2.2.23\n\n- `New` *Shortcuts* — copy and cut Blocks selected by CMD+A\n\n### 2.2—2.7\n\n- `New` *Sanitize API* — [Sanitize Config](https://github.com/codex-team/editor.js/blob/master/docs/tools.md#automatic-sanitize) of `Block Tools` now automatically extends by tags of `Inline Tools` that is enabled by current Tool by `inlineToolbar` option. You don't need more to specify `a, b, mark, code` manually. This feature will be added to fields that supports inline markup.\n- `New` *Block Selection* — Ability to select Block by `CMD+A`, and the whole Editor by double `CMD+A`. After that, you can copy (`CMD+C`), remove (`Backspace`) or clear (`Enter`) selected Blocks.\n- `New` *[Styles API](https://github.com/codex-team/editor.js/blob/master/types/api/styles.d.ts)* — Added `button` class for stylization of any buttons provided by Tools with one unified style.\n- `New` *[Notifier API](https://github.com/codex-team/editor.js/blob/master/docs/api.md#notifierapi)* — methods for showing user notifications: on success, errors, warnings, etc.\n- `New` *Block Tool* — [Table](http://github.com/editor-js/table) constructor 💪\n- `New` If one of the Tools is unavailable on Editor initialization, its Blocks will be rendered with *Dummy Block*, describing that user can not edit content of this Block. Dummy Blocks can be moved, removed and saved as normal Blocks. So saved data won't be lost if one of the Tools is failed\n- `New` [Public TS-types](https://github.com/codex-team/editor.js/tree/master/types) are presented.\n- `Changes` *Tools API*  — options `irreplaceable` and `contentless` was removed.\n- `Changes` *Tools API* — [Paste API](https://github.com/codex-team/editor.js/blob/master/docs/tools.md#paste-handling): tags, patterns and mime-types now should be specified by Tool's `pasteConfig` static property. Custom Paste Event should be handled by `onPaste(event)` that should not be static from now.\n- `Changes` *Tools API* — options `displayInToolbox ` and `toolboxIcon` was removed. Use [`toolbox`](https://github.com/codex-team/editor.js/blob/master/docs/tools.md#internal-tool-settings) instead, that should return object with `icon` and `title` field, or `false` if Tool should not be placed at the Toolbox. Also, there are a way to override `toolbox {icon, title}` settings provided by Tool with you own settings at the Initial Config.\n- `Improvements` — All Projects code now on TypeScript\n- `Improvements` — NPM package size decreased from 1300kb to 422kb\n- `Improvements` — Bundle size decreased from 438kb to 252kb\n- `Improvements` — `Inline Toolbar`: when you add a Link to the selected fragment, Editor will highlight this fragment even when Caret is placed into the URL-input.\n- `Improvements` — Block Settings won't be shown near empty Blocks of `initialType` by default. You should click on them instead.\n- `Improvements` — `onChange`-callback now will be fired even with children attributes changing.\n- `Improvements` — HTMLJanitor package was updated due to found vulnerability\n- `Improvements` — Logging improved: now all Editor's logs will be preceded by beautiful label with current Editor version.\n- `Improvements` — Internal `isEmpty` checking was improved for Blocks with many children nodes (200 and more)\n- `Improvements` — Paste improvements: tags that can be substituted by Tool now will matched even on deep-level of pasted DOM three.\n- `Improvements` — There is no more «unavailable» sound on copying Block by `CMD+C` on macOS\n- `Improvements` — Dozens of bugfixes and small improvements\n\nSee a whole [Changelog](/docs/)\n\n### 2.1-beta changelog\n\n- `New` *Tools API* — support pasted content via drag-n-drop or from the Buffer. See [documentation](https://github.com/codex-team/editor.js/blob/master/docs/tools.md#paste-handling) and [example](https://github.com/editor-js/simple-image/blob/master/src/index.js#L177) at the Simple Image Tool.\n- `New` *Tools API* — new `sanitize` getter for Tools for automatic HTML sanitizing of returned data. See [documentation](https://github.com/codex-team/editor.js/blob/master/docs/tools.md#sanitize) and [example](https://github.com/editor-js/paragraph/blob/master/src/index.js#L121) at the Paragraph Tool\n- `New` Added `onChange`-callback, fired after any modifications at the Editor. See [documentation](https://github.com/codex-team/editor.js/blob/master/docs/installation.md#features).\n- `New` New Inline Tool example — [Marker](https://github.com/editor-js/marker)\n- `New` New Inline Tool example — [Code](https://github.com/editor-js/code)\n- `New` New [Editor.js PHP](http://github.com/codex-team/codex.editor.backend) — example of server-side implementation with HTML purifying and data validation.\n- `Improvements` - Improvements of Toolbar's position calculation.\n- `Improvements` — Improved zero-configuration initialization.\n- and many little improvements.\n"
  },
  {
    "path": "docs/api.md",
    "content": "# Editor.js API\n\n---\nMost actual API described by [this interface](../types/api/index.d.ts).\n\n---\n📃 See official API documentation [https://editorjs.io/api](https://editorjs.io/api)\n\n---\n\nTools have access to the public methods provided by Editor.js API Module. Plugin and Tune Developers\ncan use Editor\\`s API as they want.\n\n## Block API\n\nAPI for certain Block methods and properties. You can access it through `editor.api.block.getBlockByIndex` method or get it form `block` property of [Tool constructor](../types/tools/block-tool.d.ts) argument.\n\n`name: string` — Block's Tool name (key, specified in `tools` property of initial configuration)\n\n`config: ToolConfig` — Tool config passed on Editor initialization\n\n`holder: HTMLElement` — HTML Element that wraps Tool's HTML content\n\n`isEmpty: boolean` — `true` if Block has any editable content\n\n`selected: boolean` - `true` if Block is selected with Cross-Block Selection\n\n`set stretched(state: boolean)` — set Block's stretch state\n\n`stretched: boolean` — `true` if Block is stretched\n\n`call(methodName: string, param?: object): void` — method to call any Tool's instance methods with checks and error handlers under-the-hood. For example, [Block lifecycle hooks](./tools.md#block-lifecycle-hooks)\n\n`save(): Promise<void|SavedData>` — returns data saved from current Block's state, including Tool name and saving exec time\n\n`validate(data: BlockToolData): Promise<boolean>` — calls Tool's validate method if exists\n\n`dispatchChange(): void` - Allows to say Editor that Block was changed. Used to manually trigger Editor's 'onChange' callback. Can be useful for block changes invisible for editor core.\n\n## Api object description\n\nCommon API interface.\n\n```js\nexport interface API {\n   blocks: IBlocksAPI;\n   caret: ICaretAPI;\n   sanitizer: ISanitizerAPI;\n   toolbar: IToolbarAPI;\n   // ...\n }\n ```\n\n#### BlocksAPI\n\nMethods that working with Blocks\n\n`render(data)` - render passed JSON data\n\n`renderFromHTML(data)` - parse and render passed HTML string (*not for production use*)\n\n`swap(fromIndex, toIndex)` - swaps two Blocks by their positions (deprecated:\nuse 'move' instead)\n\n`move(toIndex, fromIndex)` - moves block from one index to another position.\n`fromIndex` will be the current block's index by default.\n\n`delete(blockIndex?: Number)` - deletes Block with passed index\n\n`getCurrentBlockIndex()` - current Block index\n\n`getBlockByIndex(index: Number)` - returns Block API object by passed index\n\n`getBlocksCount()` - returns Blocks count\n\n`stretchBlock(index: number, status: boolean)` - _Deprecated. Use Block API interface instead._ make Block stretched.\n\n`insertNewBlock()` - __Deprecated__ insert new Block after working place\n\n`insert(type?: string, data?: BlockToolData, config?: ToolConfig, index?: number, needToFocus?: boolean)` - insert new Block with passed parameters\n\n`update(id: string, data?: BlockToolData, tunes?: {[name: string]: BlockTuneData})` - updates block data and block tunes for the block with passed id\n\n#### SanitizerAPI\n\n`clean(taintString, config)` - method uses HTMLJanitor to clean taint string.\n\nEditor.js provides basic config without attributes, but you can inherit by passing your own config.\n\nIf Tool enables inline-tools, we get it's sanitizing rules and merge with your passed custom rules.\n\nUsage:\n\n```js\nlet taintString = '<div><p style=\"font-size: 5em;\"><b></b>BlockWithText<a onclick=\"void(0)\"></div>'\nlet customConfig = {\n  b: true,\n  p: {\n    style: true,\n  },\n}\nthis.api.sanitizer.clean(taintString, customConfig);\n```\n\n### ToolbarAPI\n\nMethods that working with Toolbar\n\n`open()` - opens toolbar\n\n`close()` - closes toolbar, toolbox and blockSettings if they are opened\n\n### InlineToolbarAPI\n\nMethods that works with inline toolbar\n\n`open()` - opens inline toolbar, (opens for the current selection)\n\n`close()` - closes inline toolbar\n\n### ListenerAPI\n\nMethods that allows to work with DOM listener. Useful when you forgot to remove listener. Module collects all listeners and destroys automatically\n\n`on(element: HTMLElement, eventType: string, handler: Function, useCapture?: boolean)` - add event listener to HTML element\n\n`off(element: HTMLElement, eventType: string, handler: Function)` - remove event handler from HTML element\n\n\n### CaretAPI\n\nMethods to manage caret position.\n\nEach method accept `position` and `offset` parameters. `Offset` should be used to shift caret by passed amount of characters.\n\n`Position` can be one of the following values:\n\n| Value     | Description\n| --------- | -----------\n| `start`   | Caret will be set at the Block's beginning\n| `end`     | Caret will be set at the Block end\n| `default` | More or less emulates browser behaviour, in most cases behaves as `start`\n\nEach method returns `boolean` value: true if caret is set successfully or false otherwise (e.g. when there is no Block at index);\n\n`setToFirstBlock(position?: 'end'|'start'|'default', offset?: number): boolean;` — set caret to the first Block\n\n`setToLastBlock(position?: 'end'|'start'|'default', offset?: number): boolean;` — set caret to the last Block\n\n`setToNextBlock(position?: 'end'|'start'|'default', offset?: number): boolean;` — set caret to the next Block\n\n`setToPreviousBlock(position?: 'end'|'start'|'default', offset?: number): boolean;` — set caret to the previous Block\n\n`setToBlock(index: number, position?: 'end'|'start'|'default', offset?: number): boolean;` — set caret to the Block by passed `index`\n\n`focus(atEnd?: boolean): boolean;` — set caret to the Editor. If `atEnd` is true, set it at the end.\n\n### NotifierAPI\n\nIf you need to show any messages for success or failure events you can use notifications module.\n\nCall on target Editor:\n\n```javascript\nlet editor = new EditorJS({\n  onReady: () => {\n    editor.notifier.show({\n      message: 'Editor is ready!'\n    });\n  },\n});\n```\n\nIn Tool's class:\n\n```javascript\nthis.api.notifier.show({\n  message: 'Cannot upload image. Wrong mime-type.',\n  style: 'error',\n});\n```\n\n![](assets/14fcdbe4-d6eb-41d4-b66e-e0e86ccf1a4b.jpg)\n\n\nCheck out [`codex-notifier` package page](https://github.com/codex-team/js-notifier) on GitHub to find docs, params and examples.\n\n### Destroy API\n\nIf there are necessity to remove Editor.js instance from the page you can use `destroy()` method.\n\nIt makes following steps:\n\n1. Clear the holder element by setting it\\`s innerHTML to empty string\n\n2. Remove all event listeners related to Editor.js\n\n3. Delete all properties from instance object and set it\\`s prototype to `null`\n\nAfter executing the `destroy` method, editor inctance becomes an empty object. This way you will free occupied JS Heap on your page.\n\n### Tooltip API\n\nMethods for showing Tooltip helper near your elements. Parameters are the same as in [CodeX Tooltips](http://github.com/codex-team/codex.tooltips) lib.\n\n#### Show\n\nMethod shows tooltip with custom content on passed element\n\n```js\nthis.api.tooltip.show(element, content, options);\n```\n\n| parameter | type | description |\n| -- | -- | -- |\n| `element` | _HTMLElement_ | Tooltip will be showed near this element |\n| `content` | _String_ or _Node_ | Content that will be appended to the Tooltip |\n| `options` | _Object_ | Some displaying options, see below |\n\nAvailable showing options\n\n| name | type | action |\n| -- | -- | -- |\n| placement | `top`, `bottom`, `left`, `right` | Where to place the tooltip. Default value is `bottom' |\n| marginTop | _Number_ | Offset above the tooltip with `top` placement |\n| marginBottom | _Number_ | Offset below the tooltip with `bottom` placement |\n| marginLeft | _Number_ | Offset at left from the tooltip with `left` placement |\n| marginRight | _Number_ | Offset at right from the tooltip with `right` placement |\n| delay | _Number_ | Delay before showing, in ms. Default is `70` |\n| hidingDelay | _Number_ | Delay before hiding, in ms. Default is `0` |\n\n#### Hide\n\nMethod hides the Tooltip.\n\n```js\nthis.api.tooltip.hide();\n```\n\n#### onHover\n\nDecorator for showing tooltip near some element by \"mouseenter\" and hide by \"mouseleave\".\n\n```js\nthis.api.tooltip.onHover(element, content, options);\n```\n\n### API Shorthands\n\nEditor`s API provides some shorthands for API methods.\n\n| Alias    | Method          |\n| ------   | --------------- |\n| `clear`  | `blocks.clear`  |\n| `render` | `blocks.render` |\n| `focus`  | `caret.focus`   |\n| `save`   | `saver.save`    |\n\n> Example\n\n```javascript\nconst editor = EditorJS();\n\neditor.focus();\neditor.save();\n```\n\n"
  },
  {
    "path": "docs/block-tunes.md",
    "content": "# Block Tunes\n\nSimilar with [Tools](tools.md) represented Blocks, you can create Block Tunes and connect it to particular Tool or for all Tools.\n\nBlock Tunes allows you to set any additional options to Blocks. For example, with corresponded Block Tunes you can mark Block as «spoiler», give it an anchor, set a background, and so on.\n\n## Base structure\n\nTune's class should have the `isTune` property (static getter) set to `true`.\n\nBlock Tune must implement the `render()` method which returns an HTML Element that will be appended to the Block Settings panel.\n\n- `render()` — create a button\n\nAlso, you can provide optional methods\n\n- `wrap()` — wraps Block content with own HTML elements\n- `save()` — save Tunes state on Editor's save\n\nAt the constructor of Tune's class exemplar you will receive an object with following parameters:\n\n| Parameter | Description |\n| --------- | ----------- |\n| api  | Editor's [API](api.md) obejct |\n| config | Configuration of Block Tool Tune is connected to (might be useful in some cases) |\n| block | [Block API](api.md#block-api) methods for block Tune is connected to |\n| data | Saved Tune data |\n\n---\n\n### render(): HTMLElement\n\nMethod that returns button to append to the block settings area\n\n#### Parameters\n\nMethod does not accept any parameters\n\n#### Return value\n\ntype | description |\n-- | -- |\n`HTMLElement` | element that will be added to the block settings area |\n\n---\n\n### wrap(blockContent: HTMLElement): HTMLElement\n\nMethod that accepts Block's content and wrap it with your own layout.\nMight be useful if you want to modify Block appearance.\n\n```javascript\nclass Tune {\n    wrap(blockContent) {\n        const myWrapper = document.createElement('div');\n\n        myWrapper.append(blockContent);\n\n        return myWrapper;\n    }\n}\n```\n\n#### Parameters\n\nname | type | description |\n-- |-- | -- |\nblockContent | HTMLElement | Block's content (might be wrapped by other Tunes) |\n\n#### Return value\n\n| type | description |\n| -- | -- |\n| HTMLElement | Your element that wraps block content |\n\n---\n\n### save()\n\nMethod should return Tune's state you want to save to Editor's output\n\n#### Parameters\n\nNo parameters\n\n#### Return value\n\ntype | description |\n-- | -- |\n`any` | any data you want to save |\n\n---\n\n### static prepare()\n\nIf you need to prepare some data for Tune (eg. load external script, create HTML nodes in the document, etc) you can use the static `prepare()` method.\n\nIt accepts tunes config passed on Editor's initialization as an argument:\n\n\n```javascript\nclass Tune {\n  static prepare(config) {\n    loadScript();\n    insertNodes();\n    ...\n  }\n}\n```\n\n#### Parameters\n\ntype | description |\n-- | -- |\n`object` | your Tune configuration |\n\n\n#### Return value\n\nNo return value\n\n---\n\n### static reset()\n\nOn Editor destroy you can use an opposite method `reset` to clean up all prepared data:\n\n```javascript\nclass Tune {\n  static reset() {\n    cleanUpScripts();\n    deleteNodes();\n  ...\n  }\n}\n```\n\n#### Parameters\n\nNo parameters\n\n#### Return value\n\nNo return value\n\n---\n\n### static get sanitize()\n\nIf your Tune inserts any HTML markup into Block's content you need to provide sanitize configuration, so your HTML is not trimmed on save.\n\nPlease see more information at [sanitizer page](sanitizer.md).\n\n\n```javascript\nclass Tune {\n  static get sanitize() {\n    return {\n      sup: true\n    }\n  }\n}\n```\n\n## Format\n\nTunes data is saved to `tunes` property of output object:\n\n```\n{\n  blocks: [\n    {\n      type: 'paragraph',\n      data: {\n        text: 'This is paragraph with Tune'\n      },\n      tunes: {\n        'my-tune-name': {},\n        favorite: true,\n        anchor: 'might be string'\n      }\n    }\n  ]\n}\n```\n"
  },
  {
    "path": "docs/caret.md",
    "content": "# Editor.js Caret Module\n\nThe `Caret` module contains methods working with caret. Uses [Range](https://developer.mozilla.org/en-US/docs/Web/API/Range) methods to navigate caret\nbetween blocks. \n\nCaret class implements basic Module class that holds User configuration\nand default Editor.js instances\n\n## Properties\n\n## Methods\n\n### setToBlock\n\n```javascript\nCaret.setToBlock(block, position, offset)\n```\n\n> Method gets Block instance and puts caret to the text node with offset\n\n#### params\n\n| Param        | Type | Description|\n| -------------|------ |:-------------:|\n| block        | Object | Block instance that BlockManager created|\n| position     | String | Can be 'start', 'end' or 'default'. Other values will be treated as 'default'. Shows position of the caret regarding to the Block.|\n| offset       | Number | caret offset regarding to the text node (Default: 0)|\n\n\n### setToTheLastBlock\n\n```javascript\nCaret.setToTheLastBlock()\n```\n\n> sets Caret at the end of last Block\nIf last block is not empty, inserts another empty Block which is passed as initial\n"
  },
  {
    "path": "docs/installation.md",
    "content": "# Installation Guide\n\nThere are few steps to run Editor.js on your site.\n\n1. [Load Editor's core](#load-editors-core)\n2. [Load Tools](#load-tools)\n3. [Initialize Editor's instance](#create-editor-instance)\n\n## Load Editor's core\n\nFirstly you need to get Editor.js itself. It is a [minified script](../dist/editor.js) with minimal available\n\nChoose the most usable method of getting an Editor for you.\n\n- Node package\n- Source from CDN\n- Local file from a project\n\n### Node.js\n\nInstall the package via NPM or Yarn\n\n```shell\nnpm i @editorjs/editorjs\n```\n\nInclude module at your application\n\n```javascript\nimport EditorJS from '@editorjs/editorjs';\n```\n\n### Use from CDN\n\nYou can load specific version of package from [jsDelivr CDN](https://www.jsdelivr.com/package/npm/@editorjs/editorjs).\n\n`https://cdn.jsdelivr.net/npm/@editorjs/editorjs@2.10.0`\n\nThen require this script.\n\n```html\n<script src=\"...\"></script>\n```\n\n### Save sources to project\n\nCopy [editor.js](../dist/editor.js) file to your project and load it.\n\n```html\n<script src=\"editor.js\"></script>\n```\n\n## Load Tools\n\nEach Block at the Editor.js represented by [Tools](tools.md). There are simple external scripts with their own logic. You'll probably want to use several Block Tools that should be connected.\n\nFor example, check out our [Header](https://github.com/editor-js/header) Tool that represents heading blocks.\n\nYou can install the Header Tool via the same ways as an Editor (Node.js, CDN, local file).\n\nCheck [Editor.js's community](https://github.com/editor-js/) to see Tools examples.\n\n**Example:** use Header from CDN\n\n```html\n<script src=\"https://cdn.jsdelivr.net/npm/codex.editor.header@2.1.0/dist/bundle.js\"></script>\n```\n\n## Create Editor instance\n\nCreate an instance of Editor.js and pass [Configuration Object](../src/types-internal/editor-config.ts).\nAt least the `holder` option is required.\n\n```html\n<div id=\"editorjs\"></div>\n```\n\nYou can create a simple Editor only with a default Paragraph Tool by passing a string with element's Id (wrapper for Editor) as a configuration param or use default `editorjs`.\n\n```javascript\nvar editor = new EditorJS(); /** Zero-configuration */\n\n// equals\n\nvar editor = new EditorJS('editorjs');\n````\n\nOr pass a whole settings object.\n\n```javascript\nvar editor = new EditorJS({\n    /**\n     * Create a holder for the Editor and pass its ID\n     */\n    holder : 'editorjs',\n\n    /**\n     * Available Tools list.\n     * Pass Tool's class or Settings object for each Tool you want to use\n     */\n    tools: {\n        header: {\n          class: Header,\n          inlineToolbar : true\n        },\n        // ...\n    },\n\n    /**\n     * Previously saved data that should be rendered\n     */\n    data: {}\n});\n```\n\n## Ready callback\n\nEditor.js needs a bit of time to initialize. It is an asynchronous action so it won't block execution of your main script.\n\nIf you need to know when the editor instance is ready you can use one of the following ways:\n\n##### Pass `onReady` property to the configuration object.\n\nIt must be a function:\n\n```javascript\nvar editor = new EditorJS({\n   // Other configuration properties\n\n   /**\n    * onReady callback\n    */\n   onReady: () => {console.log('Editor.js is ready to work!')}\n});\n```\n\n#### Use `isReady` promise.\n\nAfter you create a new `EditorJS` object it will contain `isReady` property.\nIt is a Promise object that resolves when the editor will be ready to work and rejected otherwise.\nIf there is an error during initialization `isReady` promise will be rejected with an error message.\n\n```javascript\nvar editor = new EditorJS();\n\neditor.isReady\n  .then(() => {\n    /** Do anything you need after editor initialization */\n  })\n  .catch((reason) => {\n    console.log(`Editor.js initialization failed because of ${reason}`)\n  });\n```\n\nYou can use `async/await` to keep your code looking synchronous:\n\n```javascript\nvar editor = new EditorJS();\n\ntry {\n  await editor.isReady;\n  /** Do anything you need after editor initialization */\n} catch (reason) {\n  console.log(`Editor.js initialization failed because of ${reason}`)\n}\n```\n\n\n## Saving Data\n\nCall `editor.saver.save()` and handle returned Promise with saved data.\n\n```javascript\neditor.saver.save()\n  .then((savedData) => {\n    console.log(savedData);\n  });\n```\n\n## Features\n\nAlso, Editor.js provides useful methods to work with Editor's state.\n\n```javascript\nvar editor = new EditorJS({\n   // Other configuration properties\n\n   /**\n    * onReady callback\n    */\n   onReady: () => {console.log('Editor.js is ready to work!')},\n\n   /**\n    * onChange callback\n    * Accepts CustomEvent describing what happened\n    */\n   onChange: (editorAPI, event) => {console.log('Now I know that Editor\\'s content changed!')}\n});\n```\n\n## Example\n\nTake a look at the [example.html](../example/example.html) to view more detailed examples.\n"
  },
  {
    "path": "docs/releases.md",
    "content": "# Branches, versions and releases — complete guideline\n\n## Branches\n\nThe project has two main branches: `master` and `next`.\n\nBranch `master` contains the latest stable version of the editor.\nThe latest version published to NPM available by default or by the tag `latest`.\n\nBranch `next` used for development the next (release candidate) version of the editor.\nIt may contain bug fixes, improvements or features. This version is available in NPM by `next` tag.\n\n## Versions\n\nWe use [semantic versioning](https://semver.org) as a main guide for naming updates.\n\n`<major>.<minor>.<patch>`\n\nYou need to bump the part of version according the changes:\n\n- `patch` — for bug fixes, docs updates, code style fixes and other changes which do not affect the result project bundle\n- `minor` — for new features with no backward compatibility problems.\n- `major` — for breaking changes without backward compatibility with the api of the previous version of the project.\n\nPre-release versions may contain additional `-rc.*` suffix.\n\n## Release publishing\n\nDrafts for new releases are created automatically via [create-a-release-draft.yml](.github/workflows/create-a-release-draft.yml)\nworkflow when pull request to `next` branch was merged with an updated version in the package.json file.\n\nThere is a [workflow](.github/workflows/publish-package-to-npm.yml) that fired on a new release publishing on GitHub.\n\nUse target version changelog as a description.\n\n![](assets/57267bab-f2f0-411b-a9d1-69abee6abab5.jpg)\n\nThen you can publish the release and wait for package publishing via action.\n\nThis package version will be published to NPM with default `latest` tag.\n\n### Release candidate publishing\n\nIf you want to publish release candidate version, use suffix `-rc.*` for package\nversion in package.json file and in tag on releases page. Workflow will detect it and mark a release as \"pre-release\".\n\n![](assets/796de9eb-bbe0-485c-bc8f-9a4cb76641b7.jpg)\n\nThis package version will be published to NPM with `next` tag.\n\nStable version: `2.19.0`\nRelease candidate: `2.19.1-rc.0`, `2.19.1-rc.1`, ...\nNext version: `2.19.1`\n\n## Auto-bump version\n\nAfter each PR merge to the `next` branch [bump-version-on-merge-next.yml](.github/workflows/bump-version-on-merge-next.yml)\nworkflow will check if a package version was updated. If there is no update then it will open a new PR with a next\nprerelease version.\n\n### How it works\n\nThe command for bumping a version will be running in a workflow.\n\n`yarn version --prerelease --preid rc --no-git-tag-version`\n\nPrerelease version will be bumped or a new prerelease patch will be created:\n\n- `2.19.1` -> `2.19.2-rc.0`\n- `2.19.2-rc.0` -> `2.19.2-rc.1`\n\n### Change version\n\nYou can edit version (and PR name of course) if you need to publish not a pre-release version or any other.\n\nIf the next update is planned to raise the minor version (`2.19.1` -> `2.20.0`), then change it before version update merge.\n\n- `2.19.1` will be bumped to `2.19.2-rc.0` be default, change `2.19.2-rc.0` to `2.20.0-rc.0`\n\n### Ignore update\n\nIf you do not need to upgrade and publish the update with the merged pull request (docs update or any other non-important changes),\nyou can close the pull request generated by the workflow.\n\n## Example pipeline\n\nLet's imagine that package version is `2.19.0` and you want to add some bug fixes and publish an update as `2.19.1`.\n\n1. Merge a single update or a few pulls with fixes to the default branch `next`.\n2. Workflow [bump-version-on-merge-next.yml](.github/workflows/bump-version-on-merge-next.yml) will bump the version up\nto `2.19.1-rc.0` in the package.json and open a new pull request.\n3. After bump version PR merge, the workflow [create-a-release-draft.yml](.github/workflows/create-a-release-draft.yml)\nwill automatically create a draft release on GitHub.\n4. Check this new draft release on the releases page. Check tag `v2.19.1-rc.0` and notice \"This is pre-release\" checkbox\nif it should be for a release candidate versions. Then publish that release.\n5. [Workflow](.github/workflows/publish-package-to-npm.yml) will automatically push the package to NPM with tag `next`.\n6. When you ready to publish a release, remove suffix from version name in package.json (`2.19.1-rc.0` -> `v2.19.1`)\nin pull request from workflow [bump-version-on-merge-next.yml](.github/workflows/bump-version-on-merge-next.yml).\nFollow steps 3-5 with workflows and publish a new version as `latest` update.\n7. Merge branch `next` to `master` and save sources for history.\n"
  },
  {
    "path": "docs/sanitizer.md",
    "content": "# Editor.js Sanitizer Module\n\nThe `Sanitizer` module represents a set of methods that clears taint strings.\nUses lightweight npm package with simple API [html-janitor](https://www.npmjs.com/package/html-janitor)\n\n## Methods \n\n### clean\n\n```javascript\nclean(taintString, customConfig)\n```\n\n> Cleans up the passed taint string\n \n#### params\n\n| Param        | Type | Description|\n| -------------|------ |:-------------:|\n| taintString  | String | string that needs to be cleaned|\n| customConfig | Object | Can be passed new config per usage (Default: uses default configuration)|\n\n"
  },
  {
    "path": "docs/toolbar-settings.md",
    "content": "# Editor.js Toolbar Block Settings Module\n\nToolbar Module has space for Block settings. Settings divided into:\n - space for plugin's settings, that is described by «Plugin»'s Developer\n - space for default settings. This option is also can be implemented and expanded\n\nThey difference between zones is that the first option is specified by plugin\nand each Block can have different options, when second option is for every Block\nregardless to the plugin's option.\n\n### Let's look the examples:\n\n«Plugin»'s Developers need to expand «renderSettings» method that returns HTML.\nEvery user action will be handled by itself. So, you can easily write\ncallbacks that switches your content or makes better. For more information\nread [Tools](tools.md).\n\n---\n\n«Tune»'s Developers need to implement core-provided interface to develop\ntunes that will be appeared in Toolbar default settings zone.\n\nTunes must expand two important methods:\n - `render()` - returns HTML and it is appended to the default settings zone\n - `save()` - extracts important information to be saved\n\nNo restrictions. Handle user action by yourself\n\nCreate Class that implements block-tune.ts\n\nYour Tune's constructor gets argument as object and it includes:\n - {Object} api - object contains public methods from modules. @see [API](api.md)\n - {Object} settings - settings contains block default state.\nThis object could have information about cover, anchor and so on.\n\nExample on TypeScript:\n\n```js\n\nimport IBlockTune from './block-tune';\n\nexport default class YourCustomTune implements IBlockTune {\n\n  public constructor({api, settings}) {\n    this.api = api;\n    this.settings = settings;\n  }\n\n  render() {\n    let someHTML = '...';\n    return someHTML;\n  }\n\n  save() {\n    // Return the important data that needs to be saved\n    return object\n  }\n\n  someMethod() {\n    // moves current block down\n    this.api.blocks.moveDown();\n  }\n}\n```\n\nExample on ES6\n\n```js\nexport default class YourCustomTune {\n\n  constructor({api, settings}) {\n    this.api = api;\n    this.settings = settings;\n  }\n\n  render() {\n    let someHTML = '...';\n    return someHTML;\n  }\n\n  save() {\n    // Return the important data that needs to be saved\n    return object\n  }\n\n  someMethod() {\n    // moves current block down\n    this.api.blocks.moveDown();\n  }\n}\n```\n"
  },
  {
    "path": "docs/tools-inline.md",
    "content": "# Tools for the Inline Toolbar\n\nSimilar with [Tools](tools.md) represented Blocks, you can create Tools for the Inline Toolbar. It will work with \nselected fragment of text. The simplest example is `bold` or `italic` Tools.\n\n## Base structure\n\nFirst of all, Tool's class should have a `isInline` property (static getter) set as `true`. \n\nAfter that Inline Tool should implement next methods.\n\n- `render()` — create a button\n- `surround()` — works with selected range\n- `checkState()` — get Tool's activated state by selected range\n\nAlso, you can provide optional methods\n\n- `renderActions()` — create additional element below the buttons\n- `clear()` — clear Tool's stuff on opening/closing of Inline Toolbar\n- `sanitize()` — sanitizer configuration\n\nAt the constructor of Tool's class exemplar you will accept an object with the [API](api.md) as a parameter.\n\n---\n\n### render()\n\nMethod that returns button to append at the Inline Toolbar\n\n#### Parameters\n\nMethod does not accept any parameters\n\n#### Return value\n\ntype | description | \n-- | -- |\n`HTMLElement` | element that will be added to the Inline Toolbar |\n\n---\n\n### surround(range: Range)\n\nMethod that accepts selected range and wrap it somehow\n\n#### Parameters\n\nname | type | description | \n-- |-- | -- |\nrange | Range | first range of current Selection |\n\n#### Return value\n\nThere is no return value\n\n---\n\n### checkState(selection: Selection)\n\nGet Selection and detect if Tool was applied. For example, after that Tool can highlight button or show some details.\n\n#### Parameters\n\nname | type | description | \n-- |-- | -- |\nselection | Selection | current Selection |\n\n#### Return value\n\ntype | description | \n-- | -- |\n`Boolean` | `true` if Tool is active, otherwise `false` |\n\n---\n\n### renderActions()\n\nOptional method that returns additional Element with actions. \nFor example, input for the 'link' tool or textarea for the 'comment' tool. \nIt will be places below the buttons list at Inline Toolbar.\n\n#### Parameters\n\nMethod does not accept any parameters\n\n#### Return value\n\ntype | description | \n-- | -- |\n`HTMLElement` | element that will be added to the Inline Toolbar |\n\n---\n\n### clear()\n\nOptional method that will be called on opening/closing of Inline Toolbar. \nCan contain logic for clearing Tool's stuff, such as inputs, states and other.\n\n#### Parameters\n\nMethod does not accept any parameters\n\n#### Return value\n\nMethod should not return a value. \n\n### static get sanitize()\n\nWe recommend to specify the Sanitizer config that corresponds with inline tags that is used by your Tool. \nIn that case, your config will be merged with sanitizer configuration of Block Tool \nthat is using the Inline Toolbar with your Tool.\n\nExample:\n\nIf your Tool wrapps selected text with `<b>` tag, the sanitizer config should looks like this:\n\n```js\nstatic get sanitize() {\n  return {\n    b: {} // {} means clean all attributes. true — leave all attributes\n  }\n}\n``` \n\nRead more about Sanitizer configuration at the [Tools#sanitize](tools.md#sanitize)\n\n### Specifying a title\n\nYou can pass your Tool's title via `title` static getter. It can be used, for example, in the Tooltip with \nicon description that appears by hover. \n\n```ts\nexport default class BoldInlineTool implements InlineTool {\n  /**\n   * Specifies Tool as Inline Toolbar Tool\n   *\n   * @return {boolean}\n   */\n  public static isInline = true;\n\n  /**\n   * Title for hover-tooltip\n   */\n  public static title: string = 'Bold';\n\n  // ... other methods\n}\n```\n"
  },
  {
    "path": "docs/tools.md",
    "content": "# Editor.js Tools\n\nEditor.js is a block-oriented editor. It means that entry composed with the list of `Blocks` of different types: `Texts`, `Headers`, `Images`, `Quotes` etc.\n\n`Tool` — is a class that provide custom `Block` type. All Tools represented by `Plugins`.\n\nEach Tool should have an installation guide.\n\n## Tool class structure\n\n### constructor()\n\nEach Tool's instance called with an params object.\n\n| Param  | Type                                                   | Description                                     |\n| ------ | ------------------------------------------------------ | ----------------------------------------------- |\n| api    | [`IAPI`](../types/index.d.ts)                          | Editor.js's API methods                         |\n| config | [`ToolConfig`](../types/tools/tool-config.d.ts)        | Special configuration params passed in «config» |\n| data   | [`BlockToolData`](../types/tools/block-tool-data.d.ts) | Data to be rendered in this Tool                |\n| block  | [`BlockAPI`](../types/api/block.d.ts)                  | Block's API methods                             |\n\n[iapi-link]: ../src/types-internal/api.ts\n\n#### Example\n\n```javascript\nconstructor({data, config, api}) {\n  this.data = data;\n  this.api = api;\n  this.config = config;\n  // ...\n}\n```\n\n### render()\n\nMethod that returns Tool's element {HTMLElement} that will be placed into Editor.\n\n### save()\n\nProcess Tool's element created by `render()` function in DOM and return Block's data.\n\n### validate(data: BlockToolData): boolean|Promise\\<boolean\\> _optional_\n\nAllows to check correctness of Tool's data. If data didn't pass the validation it won't be saved. Receives Tool's `data` as input param and returns `boolean` result of validation.\n\n### merge() _optional_\n\nMethod that specifies how to merge two `Blocks` of the same type, for example on `Backspace` keypress.\nMethod does accept data object in same format as the `Render` and it should provide logic how to combine new\ndata with the currently stored value.\n\n## Internal Tool Settings\n\nOptions that Tool can specify. All settings should be passed as static properties of Tool's class.\n\n| Name | Type | Default Value | Description |\n| -- | -- | -- | -- |\n| `toolbox` | _Object_ | `undefined` | Pass the `icon` and the `title` there to display this `Tool` in the Editor's `Toolbox` <br /> `icon` - HTML string with icon for the Toolbox <br /> `title` - title to be displayed at the Toolbox. <br /><br />May contain an array of `{icon, title, data}` to display the several variants of the tool, for example \"Ordered list\", \"Unordered list\". See details at [the documentation](https://editorjs.io/tools-api#toolbox) |\n| `enableLineBreaks` | _Boolean_ | `false` | With this option, Editor.js won't handle Enter keydowns. Can be helpful for Tools like `<code>` where line breaks should be handled by default behaviour. |\n| `isInline` | _Boolean_ | `false` | Describes Tool as a [Tool for the Inline Toolbar](tools-inline.md) |\n| `isTune` | _Boolean_ | `false` | Describes Tool as a [Block Tune](block-tunes.md) |\n| `sanitize` | _Object_ | `undefined` | Config for automatic sanitizing of saved data. See [Sanitize](#sanitize) section. |\n| `conversionConfig` | _Object_ | `undefined` | Config allows Tool to specify how it can be converted into/from another Tool. See [Conversion config](#conversion-config) section. |\n\n## User configuration\n\nAll Tools can be configured by users. You can set up some of available settings along with Tool's class\nto the `tools` property of Editor Config.\n\n```javascript\nvar editor = new EditorJS({\n  holder : 'editorjs',\n  tools: {\n    text: {\n      class: Text,\n      inlineToolbar : true,\n      // other settings..\n    },\n    header: Header\n  },\n  defaultBlock : 'text',\n});\n```\n\nThere are few options available by Editor.js.\n\n| Name | Type | Default Value | Description |\n| -- | -- | -- | -- |\n| `inlineToolbar` | _Boolean/Array_ | `false` | Pass `true` to enable the Inline Toolbar with all Tools, or pass an array with specified Tools list |\n| `config` | _Object_ | `null` | User's configuration for Plugin.\n\n## Tool prepare and reset\n\nIf you need to prepare some data for Tool (eg. load external script, create HTML nodes in the document, etc) you can use static prepare method.\n\nIt accepts tools config passed on Editor's initialization as an argument:\n\n```javascript\nclass Tool {\n  static prepare(config) {\n    loadScript();\n    insertNodes();\n    ...\n  }\n}\n```\n\nOn Editor destroy you can use an opposite method `reset` to clean up all prepared data:\n\n```javascript\nclass Tool {\n  static reset() {\n    cleanUpScripts();\n    deleteNodes();\n    ...\n  }\n}\n```\n\nBoth methods might be async.\n\n## Paste handling\n\nEditor.js handles paste on Blocks and provides API for Tools to process the pasted data.\n\nWhen user pastes content into Editor, pasted content will be splitted into blocks.\n\n1. If plain text will be pasted, it will be splitted by new line characters\n2. If HTML string will be pasted, it will be splitted by block tags\n\nAlso Editor API allows you to define your own pasting scenario. You can either:\n\n1. Specify **HTML tags**, that can be represented by your Tool. For example, Image Tool can handle `<img>` tags.\nIf tags you specified will be found on content pasting, your Tool will be rendered.\n2. Specify **RegExp** for pasted strings. If pattern has been matched, your Tool will be rendered.\n3. Specify **MIME type** or **extensions** of files that can be handled by your Tool on pasting by drag-n-drop or from clipboard.\n\nFor each scenario, you should do 2 next things:\n\n1. Define static getter `pasteConfig` in Tool class. Specify handled patterns there.\n2. Define public method `onPaste` that will handle PasteEvent to process pasted data.\n\n### HTML tags handling\n\nTo handle pasted HTML elements object returned from `pasteConfig` getter should contain following field:\n\n| Name | Type | Description |\n| -- | -- | -- |\n| `tags` | `String[]` | _Optional_. Should contain all tag names you want to be extracted from pasted data and processed by your `onPaste` method |\n\nFor correct work you MUST provide `onPaste` handler at least for `defaultBlock` Tool.\n\n#### Example\n\nHeader Tool can handle `H1`-`H6` tags using paste handling API\n\n```javascript\nstatic get pasteConfig() {\n  return {\n    tags: ['H1', 'H2', 'H3', 'H4', 'H5', 'H6'],\n  }\n}\n```\n\n**Note. Same tag can be handled by one (first specified) Tool only.**\n\n**Note. All attributes of pasted tag will be removed. To leave some attribute, you should explicitly specify them. Se below**\n\nLet's suppose you want to leave the 'src' attribute when handle pasting of the `img` tags. Your config should look like this:\n\n```javascript\nstatic get pasteConfig() {\n  return {\n    tags: [\n      {\n        img: {\n          src: true\n        }\n      }\n    ],\n  }\n}\n```\n\n[Read more](https://editorjs.io/sanitizer) about the sanitizing configuration.\n\n### RegExp patterns handling\n\nYour Tool can analyze text by RegExp patterns to substitute pasted string with data you want. Object returned from `pasteConfig` getter should contain following field to use patterns:\n\n| Name | Type | Description |\n| -- | -- | -- |\n| `patterns` | `Object` | _Optional_. `patterns` object contains RegExp patterns with their names as object's keys |\n\n**Note** Editor will check pattern's full match, so don't forget to handle all available chars in there.\n\nPattern will be processed only if paste was on `defaultBlock` Tool and pasted string length is less than 450 characters.\n\n> Example\n\nYou can handle YouTube links and insert embeded video instead:\n\n```javascript\nstatic get pasteConfig() {\n  return {\n    patterns: {\n      youtube: /http(?:s?):\\/\\/(?:www\\.)?youtu(?:be\\.com\\/watch\\?v=|\\.be\\/)([\\w\\-\\_]*)(&(amp;)?[\\w\\?‌​=]*)?/\n    },\n  }\n}\n```\n\n### Files pasting\n\nYour Tool can handle files pasted or dropped into the Editor.\n\nTo handle file you should provide `files`  property in your `pasteConfig` configuration object.\n\n`files` property is an object with the following fields:\n\n| Name | Type | Description |\n| ---- | ---- | ----------- |\n| `extensions` | `string[]` | _Optional_ Array of extensions your Tool can handle |\n| `mimeTypes` | `sring[]` | _Optional_ Array of MIME types your Tool can handle |\n\nExample\n\n```javascript\nstatic get pasteConfig() {\n  return {\n    files: {\n      mimeTypes: ['image/png'],\n      extensions: ['json']\n    }\n  }\n}\n```\n\n### Pasted data handling\n\nIf you registered some paste substitutions in `pasteConfig` property, you **should** provide `onPaste` callback in your Tool class.\n`onPaste` should be public non-static method. It accepts custom _PasteEvent_ object as argument.\n\nPasteEvent is an alias for three types of events - `tag`, `pattern` and `file`. You can get the type from _PasteEvent_ object's `type` property.\nEach of these events provide `detail` property with info about pasted content.\n\n| Type  | Detail |\n| ----- | ------ |\n| `tag` | `data` - pasted HTML element |\n| `pattern` | `key` - matched pattern key you specified in `pasteConfig` object <br /> `data` - pasted string |\n| `file` | `file` - pasted file |\n\nExample\n\n```javascript\nonPaste (event) {\n  switch (event.type) {\n    case 'tag':\n      const element = event.detail.data;\n\n      this.handleHTMLPaste(element);\n      break;\n\n    case 'pattern':\n      const text = event.detail.data;\n      const key = event.detail.key;\n\n      this.handlePatternPaste(key, text);\n      break;\n\n    case 'file':\n      const file = event.detail.file;\n\n      this.handleFilePaste(file);\n      break;\n  }\n}\n```\n\n### Disable paste handling\n\nIf you need to disable paste handling on your Tool for some reason, you can provide `false` as `pasteConfig` value.\nThat way paste event won't be processed if fired on your Tool:\n\n```javascript\nstatic get pasteConfig {\n  return false;\n}\n```\n\n## Sanitize <a name=\"sanitize\"></a>\n\nEditor.js provides [API](sanitizer.md) to clean taint strings.\nUse it manually at the `save()` method or or pass `sanitizer` config to do it automatically.\n\n### Sanitizer Configuration\n\nThe example of sanitizer configuration\n\n```javascript\nlet sanitizerConfig = {\n  b: true, // leave <b>\n  p: true, // leave <p>\n}\n```\n\nKeys of config object is tags and the values is a rules.\n\n#### Rule\n\nRule can be boolean, object or function. Object is a dictionary of rules for tag's attributes.\n\nYou can set `true`, to allow tag with all attributes or `false|{}` to remove all attributes,\nbut leave tag.\n\nAlso you can pass special attributes that you want to leave.\n\n```javascript\na: {\n  href: true\n}\n```\n\nIf you want to use a custom handler, use should specify a function\nthat returns a rule.\n\n```javascript\nb: function(el) {\n  return !el.textContent.includes('bad text');\n}\n```\n\nor\n\n```javascript\na: function(el) {\n  let anchorHref = el.getAttribute('href');\n  if (anchorHref && anchorHref.substring(0, 4) === 'http') {\n    return {\n      href: true,\n      target: '_blank'\n    }\n  } else {\n    return {\n      href: true\n    }\n  }\n}\n```\n\n### Manual sanitize\n\nCall API method `sanitizer.clean()` at the save method for each field in returned data.\n\n```javascript\nsave() {\n  return {\n    text: this.api.sanitizer.clean(taintString, sanitizerConfig)\n  }\n}\n```\n\n### Automatic sanitize\n\nIf you pass the sanitizer config as static getter, Editor.js will automatically sanitize your saved data.\n\nNote that if your Tool is allowed to use the Inline Toolbar, we will get sanitizing rules for each Inline Tool\nand merge with your passed config.\n\nYou can define rules for each field\n\n```javascript\nstatic get sanitize() {\n  return {\n    text: {},\n    items: {\n      b: true, // leave <b>\n      a: false, // remove <a>\n    }\n  }\n}\n```\n\nDon't forget to set the rule for each embedded subitems otherwise they will\nnot be sanitized.\n\nif you want to sanitize everything and get data without any tags, use `{}` or just\nignore field in case if you want to get pure HTML\n\n```javascript\nstatic get sanitize() {\n  return {\n    text: {},\n    items: {}, // this rules will be used for all properties of this object\n    // or\n    items: {\n      // other objects here won't be sanitized\n      subitems: {\n        // leave <a> and <b> in subitems\n        a: true,\n        b: true,\n      }\n    }\n  }\n}\n```\n\n## Conversion config <a name=\"conversion-config\"></a>\n\nEditor.js has a Conversion Toolbar that allows user to convert one Block to another.\n\n![](assets/6c1f708b-a30c-4ffd-a427-5b59a1a472e0.jpg)\n\n1. You can add ability to your Tool to be converted. Specify «export» property of `conversionConfig`.\n2. You can add ability to convert other Tools to your Tool. Specify «import» property of `conversionConfig`.\n\nConversion Toolbar will be shown only near Blocks that specified an «export» rule, when user selected almost all block's content.\nThis Toolbar will contain only Tools that specified an «import» rule.\n\nExample:\n\n```js\nclass Header {\n  constructor(){\n    this.data = {\n       text: '',\n       level: 2\n    }\n  }\n\n  /**\n   * Rules specified how our Tool can be converted to/from other Tool.\n   */\n  static get conversionConfig() {\n    return {\n      export: 'text', // this property of tool data will be used as string to pass to other tool\n      import: 'text' // to this property imported string will be passed\n    };\n  }\n}\n```\n\n### Your Tool -> other Tool\n\nThe «export» field specifies how to represent your Tool's data as a string to pass it to other tool.\n\nIt can be a `String` or a `Function`.\n\n`String` means a key of your Tool data object that should be used as string to export.\n\n`Function` is a method that accepts your Tool data and compose a string to export from it. See example below:\n\n```js\nclass ListTool {\n  constructor(){\n    this.data = {\n      items: [\n        'Fisrt item',\n        'Second item',\n        'Third item'\n      ],\n      type: 'ordered'\n    }\n  }\n\n  static get conversionConfig() {\n    return {\n      export: (data) => {\n        return data.items.join('.'); // in this example, all list items will be concatenated to an export string\n      },\n      // ... import rule\n    };\n  }\n}\n```\n\n### Other Tool -> your Tool\n\nThe «import» rule specifies how to create your Tool's data object from the string created by original block.\n\nIt can be a `String` or a `Function`.\n\n`String` means the key in tool data that will be filled by an exported string.\nFor example, `import: 'text'` means that `constructor` of your block will accept a `data` object with `text` property filled with string composed by original block.\n\n`Function` allows you to specify own logic, how a string should be converted to your tool data. For example:\n\n```js\nclass ListTool {\n  constructor(data){\n    this.data = data || {\n      items: [],\n      type: 'unordered'\n    }\n  }\n\n  static get conversionConfig() {\n    return {\n      // ... export rule\n\n      /**\n       * In this example, List Tool creates items by splitting original text by a dot symbol.\n       */\n      import: (string) => {\n        const items = string.split('.');\n\n        return {\n          items: items.filter( (text) => text.trim() !== ''),\n          type: 'unordered'\n        };\n      }\n    };\n  }\n}\n```\n\n## Block Lifecycle hooks\n\n### `rendered()`\n\nCalled after Block contents is added to the page\n\n### `updated()`\n\nCalled each time Block contents is updated\n\n### `removed()`\n\nCalled after Block contents is removed from the page but before Block instance deleted\n\n### `moved(MoveEvent)`\n\nCalled after Block was moved. `MoveEvent` contains `fromIndex` and `toIndex`\nrespectively.\n"
  },
  {
    "path": "docs/usage.md",
    "content": "# So how to use Editor.js\n\n## Basics\n\nEditor.js is a Block-Styled editor. Blocks is a structural units, of which the Entry is composed. \nFor example, `Paragraph`, `Heading`, `Image`, `Video`, `List` are Blocks. Each Block is represented by a Plugin. \nWe have [many](http://github.com/editor-js/) ready-to-use Plugins and the [simple API](tools.md) for creation new ones.\n\nSo how to use the Editor after [Installation](installation.md).\n\n- Create new Blocks by Enter or with the Plus Button\n- Press `TAB` or click on the Plus Button to view the Toolbox\n- Press `TAB` again to leaf Toolbox and select a Block you need. Then press Enter.\n\n\n ![](https://github.com/editor-js/list/raw/master/assets/example.gif)\n \n- Select text fragment and apply a style or insert a link from the Inline Toolbar\n\n![](assets/7ccbcfcd-1c49-4674-bea7-71021468a1bd.jpg)\n\n- Use «three-dots» button on the right to open Block Settings. From here, you can move and delete a Block \nor apply Tool's settings, if it provided. For example, set a Heading level or List style.\n\n![](assets/01a55381-46cd-47c7-b92e-34765434f2ca.jpg)   \n\n## Shortcuts\n\nWe really appreciate shortcuts. So there are few presets. \n\nAction | Shortcut | Restrictions\n-- | -- | --\n`TAB` | Show/leaf a Toolbox. | On empty block\n`SHIFT+TAB` | Leaf back a Toolbox. | While Toolbox is opened\n`ENTER` | Create a Block | While Toolbox is opened and some Tool is selected\n`CMD+B` | Bold style | On selection\n`CMD+I` | Italic style | On selection\n`CMD+K` | Insert a link | On selection\n \nAlso we support shortcuts on the all type of Tools. Specify a shortcut with the Tools configuration. For example:\n\n```js\nvar editor = new EditorJS({\n  //...\n  tools: {\n    header: {\n      class: Header,\n      shortcut: 'CMD+SHIFT+H'\n    },\n    list: {\n      class: List,\n      shortcut: 'CMD+SHIFT+L'\n    }\n  }\n  //...\n });\n\n```\n\n## Autofocus\n\nIf you want to focus Editor after page has been loaded, you can enable autofocus by passing `autofocus` to the initial config\n\n\n```js\nvar editor = new EditorJS({\n  //...\n  autofocus: true\n  //...\n });\n\n```\n\n## Holder\nThe `holder` property supports an id or a reference to dom element.\n\n```js\nvar editor = new EditorJS({\n  holder: document.querySelector('.editor'),\n})\n\nvar editor2 = new EditorJS({\n  holder: 'codex-editor' // like document.getElementById('codex-editor')\n})\n```\n\n\n\n## Placeholder\n\nBy default Editor\\`s placeholder is empty.\n\nYou can pass your own placeholder via `placeholder` field:\n\n\n```js\nvar editor = new EditorJS({\n  //...\n  placeholder: 'My awesome placeholder'\n  //...\n });\n\n```\n\nIf you are using your custom `Initial Block`, `placeholder` property is passed in `config` to your Tool constructor.\n\n## Log level\n\nYou can specify log level for Editor.js console messages via `logLevel` property of configuration:\n\n```js\nvar editor = new EditorJS({\n  //...\n  logLevel: 'WARN'\n  //..\n})\n```\n\nPossible values:\n\n| Value     | Description                  |\n| -----     | ---------------------------- |\n| `VERBOSE` | Show all messages            |\n| `INFO`    | Show info and debug messages |\n| `WARN`    | Show errors and warns only   |\n| `ERROR`   | Show errors only             |\n  \n"
  },
  {
    "path": "example/example-i18n.html",
    "content": "<!--\n  This page contains example of editor.js internalization.\n  See <script> section -> i18n property of the configuration object\n\n  \\ (•◡•) /\n-->\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <title>Editor.js 🤩🧦🤨 example</title>\n  <link href=\"https://fonts.googleapis.com/css?family=PT+Mono\" rel=\"stylesheet\">\n  <link href=\"../public/assets/demo.css\" rel=\"stylesheet\">\n  <script src=\"../public/assets/json-preview.js\"></script>\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\" />\n</head>\n<body>\n<div class=\"ce-example\">\n  <div class=\"ce-example__header\">\n    <a class=\"ce-example__header-logo\" href=\"https://codex.so/editor\">Editor.js 🤩🧦🤨</a>\n\n    <div class=\"ce-example__header-menu\">\n      <a href=\"https://github.com/editor-js\" target=\"_blank\">Plugins</a>\n      <a href=\"https://editorjs.io/usage\" target=\"_blank\">Usage</a>\n      <a href=\"https://editorjs.io/configuration\" target=\"_blank\">Configuration</a>\n      <a href=\"https://editorjs.io/creating-a-block-tool\" target=\"_blank\">API</a>\n    </div>\n  </div>\n  <div class=\"ce-example__content _ce-example__content--small\">\n    <div id=\"editorjs\"></div>\n    <div id=\"hint-core\" style=\"text-align: center;\">\n      No core bundle file found. Run <code class=\"inline-code\">yarn build</code>\n    </div>\n\n    <div class=\"ce-example__button\" id=\"saveButton\">\n      editor.save()\n    </div>\n  </div>\n  <div class=\"ce-example__output\">\n    <pre class=\"ce-example__output-content\" id=\"output\"></pre>\n\n    <div class=\"ce-example__output-footer\">\n      <a href=\"https://codex.so\" style=\"font-weight: bold;\">Made by CodeX</a>\n    </div>\n  </div>\n</div>\n\n<!-- Load Tools -->\n<script src=\"https://cdn.jsdelivr.net/npm/@editorjs/header@latest\"></script><!-- Header -->\n<script src=\"https://cdn.jsdelivr.net/npm/@editorjs/image@latest\"></script><!-- Image -->\n<script src=\"https://cdn.jsdelivr.net/npm/@editorjs/delimiter@latest\"></script><!-- Delimiter -->\n<script src=\"https://cdn.jsdelivr.net/npm/@editorjs/list@latest\"></script><!-- List -->\n<script src=\"https://cdn.jsdelivr.net/npm/@editorjs/quote@latest\"></script><!-- Quote -->\n<script src=\"https://cdn.jsdelivr.net/npm/@editorjs/code@latest\"></script><!-- Code -->\n<script src=\"https://cdn.jsdelivr.net/npm/@editorjs/embed@latest\"></script><!-- Embed -->\n<script src=\"https://cdn.jsdelivr.net/npm/@editorjs/table@latest\"></script><!-- Table -->\n<script src=\"https://cdn.jsdelivr.net/npm/@editorjs/link@latest\"></script><!-- Link -->\n<script src=\"https://cdn.jsdelivr.net/npm/@editorjs/warning@latest\"></script><!-- Warning -->\n<script src=\"https://cdn.jsdelivr.net/npm/@editorjs/marker@latest\"></script><!-- Marker -->\n<script src=\"https://cdn.jsdelivr.net/npm/@editorjs/inline-code@latest\"></script><!-- Inline Code -->\n\n<!-- Load Editor.js's Core -->\n<script src=\"../dist/editorjs.umd.js\" onload=\"document.getElementById('hint-core').hidden = true\"></script>\n\n<!-- Initialization -->\n<script>\n  /**\n   * Saving button\n   */\n  const saveButton = document.getElementById('saveButton');\n\n  /**\n   * To initialize the Editor, create a new instance with configuration object\n   * @see docs/installation.md for mode details\n   */\n  var editor = new EditorJS({\n    /**\n     * Wrapper of Editor\n     */\n    holder: 'editorjs',\n\n    /**\n     * Tools list\n     */\n    tools: {\n      paragraph: {\n        config: {\n          placeholder: \"Enter something\"\n        }\n      },\n      /**\n       * Each Tool is a Plugin. Pass them via 'class' option with necessary settings {@link docs/tools.md}\n       */\n      header: {\n        class: Header,\n        inlineToolbar: ['link'],\n        config: {\n          placeholder: 'Header'\n        },\n        shortcut: 'CMD+SHIFT+H'\n      },\n\n      /**\n       * Or pass class directly without any configuration\n       */\n      image: ImageTool,\n\n      list: {\n        class: EditorjsList,\n        inlineToolbar: true,\n        shortcut: 'CMD+SHIFT+L'\n      },\n\n      quote: {\n        class: Quote,\n        inlineToolbar: true,\n        config: {\n          quotePlaceholder: 'Enter a quote',\n          captionPlaceholder: 'Quote\\'s author',\n        },\n        shortcut: 'CMD+SHIFT+O'\n      },\n\n      warning: Warning,\n\n      marker: {\n        class:  Marker,\n        shortcut: 'CMD+SHIFT+M'\n      },\n\n      code: {\n        class:  CodeTool,\n        shortcut: 'CMD+SHIFT+C'\n      },\n\n      delimiter: Delimiter,\n\n      inlineCode: {\n        class: InlineCode,\n        shortcut: 'CMD+SHIFT+C'\n      },\n\n      linkTool: LinkTool,\n\n      embed: Embed,\n\n      table: {\n        class: Table,\n        inlineToolbar: true,\n        shortcut: 'CMD+ALT+T'\n      },\n\n    },\n    /**\n     * To provide localization of the editor.js you need to provide 'i18n' option with 'messages' dictionary:\n     *\n     * 1. At the 'ui' section of 'messages' there are translations for the internal editor.js UI elements.\n     *   You can create or find/download a dictionary for your language\n     *\n     * 2. As long as tools list is a user-specific thing (we do not know which tools you use and under which names),\n     *    so we can't provide a ready-to-use tool names dictionary.\n     *    There is a 'toolNames' section for that reason. Put translations for the names of your tools there.\n     *\n     * 3. Also, the UI of the tools you use is also invisible to editor.js core.\n     *    To pass translations for specific tools (that supports I18n API), there are 'tools' and 'blockTunes' section.\n     *    Pass dictionaries for specific plugins through them.\n     */\n    i18n: {\n      /**\n       * @type {I18nDictionary}\n       */\n      messages: {\n        /**\n         * Other below: translation of different UI components of the editor.js core\n         */\n        \"ui\": {\n          \"blockTunes\": {\n            \"toggler\": {\n              \"Click to tune\": \"Нажмите, чтобы настроить\",\n              \"or drag to move\": \"или перетащите\"\n            },\n          },\n          \"inlineToolbar\": {\n            \"converter\": {\n              \"Convert to\": \"Конвертировать в\"\n            }\n          },\n          \"toolbar\": {\n            \"toolbox\": {\n              \"Add\": \"Добавить\",\n            }\n          },\n          \"popover\": {\n            \"Filter\": \"Поиск\",\n            \"Nothing found\": \"Ничего не найдено\",\n            /**\n             * Translation of \"Convert To\"  at the Block Tunes Popover\n             */\n            \"Convert to\": \"Конвертировать в\",\n          }\n        },\n\n        /**\n         * Section for translation Tool Names: both block and inline tools\n         */\n        \"toolNames\": {\n          \"Text\": \"Параграф\",\n          \"Heading\": \"Заголовок\",\n          \"Ordered List\": \"Нумерованный список\",\n          \"Unordered List\": \"Маркированный список\",\n          \"Warning\": \"Примечание\",\n          \"Checklist\": \"Чеклист\",\n          \"Quote\": \"Цитата\",\n          \"Code\": \"Код\",\n          \"Delimiter\": \"Разделитель\",\n          \"Raw HTML\": \"HTML-фрагмент\",\n          \"Table\": \"Таблица\",\n          \"Link\": \"Ссылка\",\n          \"Marker\": \"Маркер\",\n          \"Bold\": \"Полужирный\",\n          \"Italic\": \"Курсив\",\n          \"InlineCode\": \"Моноширинный\",\n          \"Image\": \"Картинка\",\n        },\n\n        /**\n         * Section for passing translations to the external tools classes\n         */\n        \"tools\": {\n          /**\n           * Each subsection is the i18n dictionary that will be passed to the corresponded plugin\n           * The name of a plugin should be equal the name you specify in the 'tool' section for that plugin\n           */\n          \"warning\": { // <-- 'Warning' tool will accept this dictionary section\n            \"Title\": \"Название\",\n            \"Message\": \"Сообщение\",\n          },\n\n          /**\n           * Link is the internal Inline Tool\n           */\n          \"link\": {\n            \"Add a link\": \"Вставьте ссылку\"\n          },\n          /**\n           * The \"stub\" is an internal block tool, used to fit blocks that does not have the corresponded plugin\n           */\n          \"stub\": {\n            'The block can not be displayed correctly.': 'Блок не может быть отображен'\n          },\n          \"image\": {\n            \"Caption\": \"Подпись\",\n            \"Select an Image\": \"Выберите файл\",\n            \"With border\": \"Добавить рамку\",\n            \"Stretch image\": \"Растянуть\",\n            \"With background\": \"Добавить подложку\",\n          },\n          \"code\": {\n            \"Enter a code\": \"Код\",\n          },\n          \"linkTool\": {\n            \"Link\": \"Ссылка\",\n            \"Couldn't fetch the link data\": \"Не удалось получить данные\",\n            \"Couldn't get this link data, try the other one\": \"Не удалось получить данные по ссылке, попробуйте другую\",\n            \"Wrong response format from the server\": \"Неполадки на сервере\",\n          },\n          \"header\": {\n            \"Heading 1\": \"Заголовок 1\",\n            \"Heading 2\": \"Заголовок 2\",\n            \"Heading 3\": \"Заголовок 3\",\n            \"Heading 4\": \"Заголовок 4\",\n            \"Heading 5\": \"Заголовок 5\",\n            \"Heading 6\": \"Заголовок 6\",\n          },\n          \"paragraph\": {\n            \"Enter something\": \"Введите текст\"\n          },\n          \"list\": {\n            \"Ordered\": \"Нумерованный\",\n            \"Unordered\": \"Маркированный\",\n            \"Checklist\": \"Чеклист\",\n          },\n          /**\n           * Translation of \"Convert To\"  at the Inline Toolbar hint\n           */\n          \"convertTo\": {\n            \"Convert to\": \"Конвертировать в\"\n          },\n        },\n\n        /**\n         * Section allows to translate Block Tunes\n         */\n        \"blockTunes\": {\n          /**\n           * Each subsection is the i18n dictionary that will be passed to the corresponded Block Tune plugin\n           * The name of a plugin should be equal the name you specify in the 'tunes' section for that plugin\n           *\n           * Also, there are few internal block tunes: \"delete\", \"moveUp\" and \"moveDown\"\n           */\n          \"delete\": {\n            \"Delete\": \"Удалить\",\n            \"Click to delete\": \"Подтвердить удаление\"\n          },\n          \"moveUp\": {\n            \"Move up\": \"Переместить вверх\"\n          },\n          \"moveDown\": {\n            \"Move down\": \"Переместить вниз\"\n          },\n        },\n      }\n    },\n\n    /**\n     * Initial Editor data\n     */\n    data: {\n      blocks: [\n        {\n          type: \"header\",\n          data: {\n            text: \"Editor.js\",\n            level: 2\n          }\n        },\n        {\n          type : 'paragraph',\n          data : {\n            text : 'Hey. Meet the new Editor. On this page you can see it in action — try to edit this text. Source code of the page contains the example of connection and configuration.'\n          }\n        },\n        {\n          type: \"header\",\n          data: {\n            text: \"Key features\",\n            level: 3\n          }\n        },\n        {\n          type : 'list',\n          data : {\n            items : [\n              'It is a block-styled editor',\n              'It returns clean data output in JSON',\n              'Designed to be extendable and pluggable with a simple API',\n            ],\n            style: 'unordered'\n          }\n        },\n        {\n          type: \"header\",\n          data: {\n            text: \"What does it mean «block-styled editor»\",\n            level: 3\n          }\n        },\n        {\n          type : 'paragraph',\n          data : {\n            text : 'Workspace in classic editors is made of a single contenteditable element, used to create different HTML markups. Editor.js <mark class=\\\"cdx-marker\\\">workspace consists of separate Blocks: paragraphs, headings, images, lists, quotes, etc</mark>. Each of them is an independent contenteditable element (or more complex structure) provided by Plugin and united by Editor\\'s Core.'\n          }\n        },\n        {\n          type : 'paragraph',\n          data : {\n            text : `There are dozens of <a href=\"https://github.com/editor-js\">ready-to-use Blocks</a> and the <a href=\"https://editorjs.io/creating-a-block-tool\">simple API</a> for creation any Block you need. For example, you can implement Blocks for Tweets, Instagram posts, surveys and polls, CTA-buttons and even games.`\n          }\n        },\n        {\n          type: \"header\",\n          data: {\n            text: \"What does it mean clean data output\",\n            level: 3\n          }\n        },\n        {\n          type : 'paragraph',\n          data : {\n            text : 'Classic WYSIWYG-editors produce raw HTML-markup with both content data and content appearance. On the contrary, Editor.js outputs JSON object with data of each Block. You can see an example below'\n          }\n        },\n        {\n          type : 'paragraph',\n          data : {\n            text : `Given data can be used as you want: render with HTML for <code class=\"inline-code\">Web clients</code>, render natively for <code class=\"inline-code\">mobile apps</code>, create markup for <code class=\"inline-code\">Facebook Instant Articles</code> or <code class=\"inline-code\">Google AMP</code>, generate an <code class=\"inline-code\">audio version</code> and so on.`\n          }\n        },\n        {\n          type : 'paragraph',\n          data : {\n            text : 'Clean data is useful to sanitize, validate and process on the backend.'\n          }\n        },\n        {\n          type : 'delimiter',\n          data : {}\n        },\n        {\n          type : 'paragraph',\n          data : {\n            text : 'We have been working on this project more than three years. Several large media projects help us to test and debug the Editor, to make its core more stable. At the same time we significantly improved the API. Now, it can be used to create any plugin for any task. Hope you enjoy. 😏'\n          }\n        },\n        {\n          type: 'image',\n          data: {\n            file : {\n              url: 'assets/codex2x.png',\n            },\n            caption: '',\n            stretched: false,\n            withBorder: true,\n            withBackground: false,\n          }\n        },\n      ]\n    },\n    onReady: function(){\n      saveButton.click();\n    },\n  });\n\n  /**\n   * Saving example\n   */\n  saveButton.addEventListener('click', function () {\n    editor.save().then((savedData) => {\n      cPreview.show(savedData, document.getElementById(\"output\"));\n    });\n  });\n</script>\n</body>\n</html>\n"
  },
  {
    "path": "example/example-multiple.html",
    "content": "\n<!--\n Use this page for debugging purposes.\n\n This page can be used for testing multiple editor instances on the same page.\n -->\n <!DOCTYPE html>\n <html lang=\"en\">\n <head>\n   <meta charset=\"UTF-8\">\n   <title>Editor.js 🤩🧦🤨 example: Multiple instances</title>\n   <link href=\"../public/assets/demo.css\" rel=\"stylesheet\">\n   <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\" />\n </head>\n <body>\n   <div class=\"ce-example\">\n     <div class=\"ce-example__header\">\n       <a class=\"ce-example__header-logo\" href=\"https://codex.so/editor\">Editor.js 🤩🧦🤨</a>\n\n       <div class=\"ce-example__header-menu\">\n         <a href=\"https://github.com/editor-js\" target=\"_blank\">Plugins</a>\n         <a href=\"https://editorjs.io/usage\" target=\"_blank\">Usage</a>\n         <a href=\"https://editorjs.io/configuration\" target=\"_blank\">Configuration</a>\n         <a href=\"https://editorjs.io/creating-a-block-tool\" target=\"_blank\">API</a>\n       </div>\n     </div>\n     <div class=\"ce-example__content ce-example__content--with-bg _ce-example__content--small\">\n       <div id=\"hint-core\" style=\"text-align: center; padding-top: 20px\">\n         No core bundle file found. Run <code class=\"inline-code\">yarn build</code>\n       </div>\n      <div class=\"ce-example-multiple\">\n        <div id=\"editorjs1\"></div>\n        <div id=\"editorjs2\"></div>\n      </div>\n     </div>\n     <div class=\"ce-example__output\">\n       <div class=\"ce-example__output-footer\">\n         <a href=\"https://codex.so\" style=\"font-weight: bold;\">Made by CodeX</a>\n       </div>\n     </div>\n   </div>\n\n   <!-- Load Editor.js's Core -->\n   <script src=\"../dist/editorjs.umd.js\" onload=\"document.getElementById('hint-core').hidden = true\"></script>\n   <script src=\"./tools/header/dist/bundle.js\"></script><!-- Header -->\n\n   <!-- Initialization -->\n   <script>\n     /**\n      * Instance #1\n      */\n     var editor1 = new EditorJS({\n       holder: 'editorjs1',\n       tools: {\n         header: {\n           class: Header,\n           shortcut: 'CMD+SHIFT+H'\n         }\n       }\n     });\n\n     /**\n      * Instance #2\n      */\n      var editor2 = new EditorJS({\n       holder: 'editorjs2',\n       tools: {\n         header: {\n           class: Header,\n           shortcut: 'CMD+SHIFT+H'\n         }\n       }\n     });\n   </script>\n </body>\n </html>\n"
  },
  {
    "path": "example/example-popup.html",
    "content": "\n<!--\n Use this page for debugging purposes.\n\n This page can be used for testing editor nested in a popup.\n -->\n <!DOCTYPE html>\n <html lang=\"en\">\n <head>\n   <meta charset=\"UTF-8\">\n   <title>Editor.js 🤩🧦🤨 example: Popup</title>\n   <link href=\"../public/assets/demo.css\" rel=\"stylesheet\">\n   <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\" />\n </head>\n <body>\n   <div class=\"ce-example ce-example--popup\">\n     <div class=\"ce-example__header\">\n       <a class=\"ce-example__header-logo\" href=\"https://codex.so/editor\">Editor.js 🤩🧦🤨</a>\n\n       <div class=\"ce-example__header-menu\">\n         <a href=\"https://github.com/editor-js\" target=\"_blank\">Plugins</a>\n         <a href=\"https://editorjs.io/usage\" target=\"_blank\">Usage</a>\n         <a href=\"https://editorjs.io/configuration\" target=\"_blank\">Configuration</a>\n         <a href=\"https://editorjs.io/creating-a-block-tool\" target=\"_blank\">API</a>\n       </div>\n     </div>\n     <div class=\"ce-example__content ce-example__content--with-bg _ce-example__content--small\">\n       <div id=\"hint-core\" style=\"text-align: center; padding-top: 20px\">\n         No core bundle file found. Run <code class=\"inline-code\">yarn build</code>\n       </div>\n       <div class=\"stub\">\n         <h1>Base concepts</h1>\n         <p>\n           Editor.js is a block-style editor for rich media stories. It outputs clean data in JSON instead of heavy HTML markup. And more important thing is that Editor.js is designed to be API extendable and pluggable.\n         </p>\n         <p>\n           So there are a few key features:\n         </p>\n         <ul>\n           <li>Clean data output</li>\n           <li>API pluggable</li>\n           <li>Open source</li>\n         </ul>\n         <h2>\n           What does it mean block-styled\n         </h2>\n         <p>\n           In other editors, the workspace is provided by single contenteditable element in where you can create different HTML markup. All of us saw permanent bugs with moving text fragments or scaling images, while page parts are jumping and twitches. Or highlighting big parts of the text in the case when you just want to make few words to be a heading or bold.\n         </p>\n         <p>\n           The Editor.js workspace consists of separate Blocks: paragraphs, headings, images, lists, quotes, etc. Each of them is an independent contenteditable element (or more complex structure) provided by Plugin and united by Editor's Core.\n         </p>\n         <p>\n           At the same time, most useful features as arrow-navigation, copy & paste, cross block selection, and others works almost as in the familiar editors.\n         </p>\n         <h2>\n           What is clean data\n         </h2>\n         <p>\n           But the more interesting thing is, as mentioned above, that Editor.js returns clean data instead of HTML-markup. Take a look at the example.\n         </p>\n         <p>\n           If our entry consists of few paragraphs and a heading, in popular Medium editor after saving we will have something like this:\n         </p>\n         <p>\n           As you can see, there are only data we need: a list of structural Blocks with their content description.\n         </p>\n         <p>\n           You can use this data to easily render in Web, native mobile/desktop application, pass to Audio Readers, create templates for Facebook Instant Articles, AMP, RSS, create chat-bots, and many others.\n         </p>\n         <p>\n           Also, the clean data can be useful for backend processing: sanitizing, validation, injecting an advertising or other stuff, extracting Headings, make covers for social networks from Image Blocks, and other.\n         </p>\n         <h2>\n           API pluggable?\n         </h2>\n         <p>\n           A key value of the Editor is the API. All main functional units of the editor — Blocks, Inline Formatting Tools, Block Tunes — are provided by external plugins that use Editor's API.\n         </p>\n         <p>\n           We decide to extract all these Tools to separate scripts to make Editor's Core more abstract and make API more powerful. Any challenges and tasks you are facing can be implemented by your own plugins using the API.\n         </p>\n         <p>\n           At the same time, API is created to be easy-to-understand and simple-to-use.\n         </p>\n         <h2>\n           Open Source, so?\n         </h2>\n         <p>\n           Editor.js is more than just an editor. It is a big open-source community of developers and contributors. Anyone can suggest an improvement or a bug fix. Anyone can create new cool API features and plugins.\n         </p>\n         <p>\n           We will support each developer of Editor.js plugins: the best solutions will be collected to the Awesome List and promoted to the community. Together we can create a big suite of different Blocks, Inline Tools, Block Tunes that can hit a wide specter of tasks.\n         </p>\n         <p>\n           Thanks for your interest. Hope you enjoy Editor.js.\n         </p>\n       </div>\n      <div class=\"ce-example-popup\">\n        <div class=\"ce-example-popup__overlay\"></div>\n        <div class=\"ce-example-popup__popup\">\n            <div id=\"editorjs\"></div>\n        </div>\n      </div>\n     </div>\n     <div class=\"ce-example__output\">\n       <div class=\"ce-example__output-footer\">\n         <a href=\"https://codex.so\" style=\"font-weight: bold;\">Made by CodeX</a>\n       </div>\n     </div>\n   </div>\n\n   <!-- Load Editor.js's Core -->\n   <script src=\"../dist/editorjs.umd.js\" onload=\"document.getElementById('hint-core').hidden = true\"></script>\n   <script src=\"./tools/header/dist/bundle.js\"></script><!-- Header -->\n\n   <!-- Initialization -->\n   <script>\n     var editor1 = new EditorJS({\n       holder: 'editorjs',\n       tools: {\n         header: {\n           class: Header,\n           shortcut: 'CMD+SHIFT+H'\n         }\n       }\n     });\n\n   </script>\n </body>\n </html>\n"
  },
  {
    "path": "example/example-rtl.html",
    "content": "<!--\n Use this page for RTL mode debugging.\n Editor Tools are loaded as git-submodules.\n You can pull modules by running `yarn pull_tools` and start experimenting.\n -->\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <title>Editor.js RTL example</title>\n  <link href=\"https://fonts.googleapis.com/css?family=PT+Mono\" rel=\"stylesheet\">\n  <link href=\"../public/assets/demo.css\" rel=\"stylesheet\">\n  <script src=\"../public/assets/json-preview.js\"></script>\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\" />\n</head>\n<body>\n<div class=\"ce-example\">\n  <div class=\"ce-example__header\">\n    <a class=\"ce-example__header-logo\" href=\"https://codex.so/editor\">Editor.js 🤩🧦🤨</a>\n\n    <div class=\"ce-example__header-menu\">\n      <a href=\"https://github.com/editor-js\" target=\"_blank\">Plugins</a>\n      <a href=\"https://editorjs.io/usage\" target=\"_blank\">Usage</a>\n      <a href=\"https://editorjs.io/configuration\" target=\"_blank\">Configuration</a>\n      <a href=\"https://editorjs.io/creating-a-block-tool\" target=\"_blank\">API</a>\n    </div>\n  </div>\n  <div class=\"ce-example__content _ce-example__content--small\">\n    <div id=\"editorjs\"></div>\n    <div id=\"hint-core\" style=\"text-align: center;\">\n      No core bundle file found. Run <code class=\"inline-code\">yarn build</code>\n    </div>\n    <div id=\"hint-tools\" style=\"text-align: center;\">\n      No submodules found. Run <code class=\"inline-code\">yarn pull_tools && yarn tools:update</code>\n    </div>\n    <div class=\"ce-example__button\" id=\"saveButton\">\n      editor.save()\n    </div>\n  </div>\n  <div class=\"ce-example__output\">\n    <pre class=\"ce-example__output-content\" id=\"output\"></pre>\n\n    <div class=\"ce-example__output-footer\">\n      <a href=\"https://codex.so\" style=\"font-weight: bold;\">Made by CodeX</a>\n    </div>\n  </div>\n</div>\n\n<!-- Load Tools -->\n<!--\n You can upload Tools to your project's directory and use as in example below.\n Also you can load each Tool from CDN or use NPM/Yarn packages.\n Read more in Tool's README file. For example:\n https://github.com/editor-js/header#installation\n -->\n <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/header@latest\"></script><!-- Header -->\n <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/simple-image@latest\"></script><!-- Image -->\n <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/delimiter@latest\"></script><!-- Delimiter -->\n <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/nested-list@latest\"></script><!-- List -->\n <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/checklist@latest\"></script><!-- Checklist -->\n <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/quote@latest\"></script><!-- Quote -->\n <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/code@latest\"></script><!-- Code -->\n <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/embed@latest\"></script><!-- Embed -->\n <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/table@latest\"></script><!-- Table -->\n <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/link@latest\"></script><!-- Link -->\n <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/warning@latest\"></script><!-- Warning -->\n <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/raw@latest\"></script><!-- Raw -->\n\n <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/marker@latest\"></script><!-- Marker -->\n <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/inline-code@latest\"></script><!-- Inline Code -->\n\n<!-- Load Editor.js's Core -->\n<script src=\"../dist/editorjs.umd.js\" onload=\"document.getElementById('hint-core').hidden = true\"></script>\n\n<!-- Initialization -->\n<script>\n  /**\n   * Saving button\n   */\n  const saveButton = document.getElementById('saveButton');\n\n  /**\n   * To initialize the Editor, create a new instance with configuration object\n   * @see docs/installation.md for mode details\n   */\n  var editor = new EditorJS({\n    /**\n     * Wrapper of Editor\n     */\n    holder: 'editorjs',\n    i18n: {\n\n      /**\n       * Text direction\n       */\n      direction: 'rtl',\n    },\n\n    /**\n     * Tools list\n     */\n    tools: {\n      /**\n       * Each Tool is a Plugin. Pass them via 'class' option with necessary settings {@link docs/tools.md}\n       */\n      header: {\n        class: Header,\n        inlineToolbar: ['link'],\n        config: {\n          placeholder: 'Header'\n        },\n        shortcut: 'CMD+SHIFT+H'\n      },\n\n      /**\n       * Or pass class directly without any configuration\n       */\n      image: {\n        class: SimpleImage,\n        inlineToolbar: ['link'],\n      },\n\n      checklist: {\n        class: Checklist,\n        inlineToolbar: true,\n      },\n\n      quote: {\n        class: Quote,\n        inlineToolbar: true,\n        config: {\n          quotePlaceholder: 'Enter a quote',\n          captionPlaceholder: 'Quote\\'s author',\n        },\n        shortcut: 'CMD+SHIFT+O'\n      },\n\n      warning: Warning,\n\n      marker: {\n        class:  Marker,\n        shortcut: 'CMD+SHIFT+M'\n      },\n\n      code: {\n        class:  CodeTool,\n        shortcut: 'CMD+SHIFT+C'\n      },\n\n      delimiter: Delimiter,\n\n      inlineCode: {\n        class: InlineCode,\n        shortcut: 'CMD+SHIFT+C'\n      },\n\n      linkTool: LinkTool,\n\n      raw: RawTool,\n\n      embed: Embed,\n\n      table: {\n        class: Table,\n        inlineToolbar: true,\n        shortcut: 'CMD+ALT+T'\n      },\n\n    },\n\n    /**\n     * This Tool will be used as default\n     */\n    // initialBlock: 'paragraph',\n\n    /**\n     * Initial Editor data\n     */\n    data: {\n      blocks: [\n        {\n          type: \"header\",\n          data: {\n            text: \"محرر.js\",\n            level: 2\n          }\n        },\n        {\n          type : 'paragraph',\n          data : {\n            text : 'مرحبا! تعرف على المحرر الجديد. في هذه الصفحة ، يمكنك رؤيتها في العمل - حاول تحرير هذا النص.'\n          }\n        },\n        {\n          type: \"header\",\n          data: {\n            text: \"دلائل الميزات\",\n            level: 3\n          }\n        },\n        {\n          type : 'list',\n          data : {\n            items : [\n              'وهو محرر بنمط الكتلة',\n              'تقوم بإرجاع إخراج بيانات نظيفة في JSON',\n              'مصممة لتكون قابلة للتوسيع والتوصيل مع واجهة برمجة تطبيقات بسيطة'\n            ],\n            style: 'unordered'\n          }\n        }\n      ]\n    },\n    onReady: function(){\n      saveButton.click();\n    },\n    onChange: function() {\n      console.log('something changed');\n    }\n  });\n\n  /**\n   * Saving example\n   */\n  saveButton.addEventListener('click', function () {\n    editor.save().then((savedData) => {\n      cPreview.show(savedData, document.getElementById(\"output\"));\n    });\n  });\n</script>\n</body>\n</html>\n"
  },
  {
    "path": "example/example.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <title>Editor.js 🤩🧦🤨 example</title>\n  <link href=\"https://fonts.googleapis.com/css?family=PT+Mono\" rel=\"stylesheet\">\n  <link href=\"../public/assets/demo.css\" rel=\"stylesheet\">\n  <script src=\"../public/assets/json-preview.js\"></script>\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\" />\n</head>\n<body>\n  <div class=\"ce-example\">\n    <div class=\"ce-example__header\">\n      <a class=\"ce-example__header-logo\" href=\"https://codex.so/editor\">Editor.js 🤩🧦🤨</a>\n\n      <div class=\"ce-example__header-menu\">\n        <a href=\"https://github.com/editor-js\" target=\"_blank\">Plugins</a>\n        <a href=\"https://editorjs.io/usage\" target=\"_blank\">Usage</a>\n        <a href=\"https://editorjs.io/configuration\" target=\"_blank\">Configuration</a>\n        <a href=\"https://editorjs.io/creating-a-block-tool\" target=\"_blank\">API</a>\n      </div>\n    </div>\n    <div class=\"ce-example__content _ce-example__content--small\">\n      <div id=\"editorjs\"></div>\n\n      <div class=\"ce-example__button\" id=\"saveButton\">\n        editor.save()\n      </div>\n\n      <div class=\"ce-example__statusbar\">\n        Readonly:\n        <b id=\"readonly-state\">\n          Off\n        </b>\n        <div class=\"ce-example__statusbar-button\" id=\"toggleReadOnlyButton\">\n          toggle\n        </div>\n      </div>\n    </div>\n    <div class=\"ce-example__output\">\n      <pre class=\"ce-example__output-content\" id=\"output\"></pre>\n\n      <div class=\"ce-example__output-footer\">\n        <a href=\"https://codex.so\" style=\"font-weight: bold;\">Made by CodeX</a>\n      </div>\n    </div>\n  </div>\n\n  <!-- Load Tools -->\n  <!--\n   You can upload Tools to your project's directory and connect them by relative links.\n\n   Also you can load each Tool from CDN or use NPM/Yarn packages.\n\n   Read more at Tools Connection doc:\n   https://editorjs.io/getting-started#tools-connection\n   -->\n  <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/header@latest\"></script><!-- Header -->\n  <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/simple-image@latest\"></script><!-- Image -->\n  <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/delimiter@latest\"></script><!-- Delimiter -->\n  <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/list@latest\"></script><!-- List -->\n  <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/checklist@latest\"></script><!-- Checklist -->\n  <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/quote@latest\"></script><!-- Quote -->\n  <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/code@latest\"></script><!-- Code -->\n  <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/embed@latest\"></script><!-- Embed -->\n  <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/table@latest\"></script><!-- Table -->\n  <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/link@latest\"></script><!-- Link -->\n  <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/warning@latest\"></script><!-- Warning -->\n\n  <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/marker@latest\"></script><!-- Marker -->\n  <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/inline-code@latest\"></script><!-- Inline Code -->\n\n  <!-- Load Editor.js's Core -->\n  <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/editorjs@latest\"></script>\n\n  <!-- Initialization -->\n  <script>\n    /**\n     * To initialize the Editor, create a new instance with configuration object\n     * @see docs/installation.md for mode details\n     */\n    var editor = new EditorJS({\n      /**\n       * Enable/Disable the read only mode\n       */\n      readOnly: false,\n\n      /**\n       * Wrapper of Editor\n       */\n      holder: 'editorjs',\n\n      /**\n       * Common Inline Toolbar settings\n       * - if true (or not specified), the order from 'tool' property will be used\n       * - if an array of tool names, this order will be used\n       */\n      // inlineToolbar: ['link', 'marker', 'bold', 'italic'],\n      // inlineToolbar: true,\n\n      /**\n       * Tools list\n       */\n      tools: {\n        /**\n         * Each Tool is a Plugin. Pass them via 'class' option with necessary settings {@link docs/tools.md}\n         */\n        header: {\n          class: Header,\n          inlineToolbar: ['marker', 'link'],\n          config: {\n            placeholder: 'Header'\n          },\n          shortcut: 'CMD+SHIFT+H'\n        },\n\n        /**\n         * Or pass class directly without any configuration\n         */\n        image: SimpleImage,\n\n        list: {\n          class: List,\n          inlineToolbar: true,\n          shortcut: 'CMD+SHIFT+L'\n        },\n\n        checklist: {\n          class: Checklist,\n          inlineToolbar: true,\n        },\n\n        quote: {\n          class: Quote,\n          inlineToolbar: true,\n          config: {\n            quotePlaceholder: 'Enter a quote',\n            captionPlaceholder: 'Quote\\'s author',\n          },\n          shortcut: 'CMD+SHIFT+O'\n        },\n\n        warning: Warning,\n\n        marker: {\n          class:  Marker,\n          shortcut: 'CMD+SHIFT+M'\n        },\n\n        code: {\n          class:  CodeTool,\n          shortcut: 'CMD+SHIFT+C'\n        },\n\n        delimiter: Delimiter,\n\n        inlineCode: {\n          class: InlineCode,\n          shortcut: 'CMD+SHIFT+C'\n        },\n\n        linkTool: LinkTool,\n\n        embed: Embed,\n\n        table: {\n          class: Table,\n          inlineToolbar: true,\n          shortcut: 'CMD+ALT+T'\n        },\n\n      },\n\n      /**\n       * This Tool will be used as default\n       */\n      // defaultBlock: 'paragraph',\n\n      /**\n       * Initial Editor data\n       */\n      data: {\n        blocks: [\n          {\n            type: \"header\",\n            data: {\n              text: \"Editor.js\",\n              level: 2\n            }\n          },\n          {\n            type : 'paragraph',\n            data : {\n              text : 'Hey. Meet the new Editor. On this page you can see it in action — try to edit this text. Source code of the page contains the example of connection and configuration.'\n            }\n          },\n          {\n            type: \"header\",\n            data: {\n              text: \"Key features\",\n              level: 3\n            }\n          },\n          {\n            type : 'list',\n            data : {\n              items : [\n                'It is a block-styled editor',\n                'It returns clean data output in JSON',\n                'Designed to be extendable and pluggable with a simple API',\n              ],\n              style: 'unordered'\n            }\n          },\n          {\n            type: \"header\",\n            data: {\n              text: \"What does it mean «block-styled editor»\",\n              level: 3\n            }\n          },\n          {\n            type : 'paragraph',\n            data : {\n              text : 'Workspace in classic editors is made of a single contenteditable element, used to create different HTML markups. Editor.js <mark class=\\\"cdx-marker\\\">workspace consists of separate Blocks: paragraphs, headings, images, lists, quotes, etc</mark>. Each of them is an independent contenteditable element (or more complex structure) provided by Plugin and united by Editor\\'s Core.'\n            }\n          },\n          {\n            type : 'paragraph',\n            data : {\n              text : `There are dozens of <a href=\"https://github.com/editor-js\">ready-to-use Blocks</a> and the <a href=\"https://editorjs.io/creating-a-block-tool\">simple API</a> for creation any Block you need. For example, you can implement Blocks for Tweets, Instagram posts, surveys and polls, CTA-buttons and even games.`\n            }\n          },\n          {\n            type: \"header\",\n            data: {\n              text: \"What does it mean clean data output\",\n              level: 3\n            }\n          },\n          {\n            type : 'paragraph',\n            data : {\n              text : 'Classic WYSIWYG-editors produce raw HTML-markup with both content data and content appearance. On the contrary, Editor.js outputs JSON object with data of each Block. You can see an example below'\n            }\n          },\n          {\n            type : 'paragraph',\n            data : {\n              text : `Given data can be used as you want: render with HTML for <code class=\"inline-code\">Web clients</code>, render natively for <code class=\"inline-code\">mobile apps</code>, create markup for <code class=\"inline-code\">Facebook Instant Articles</code> or <code class=\"inline-code\">Google AMP</code>, generate an <code class=\"inline-code\">audio version</code> and so on.`\n            }\n          },\n          {\n            type : 'paragraph',\n            data : {\n              text : 'Clean data is useful to sanitize, validate and process on the backend.'\n            }\n          },\n          {\n            type : 'delimiter',\n            data : {}\n          },\n          {\n            type : 'paragraph',\n            data : {\n              text : 'We have been working on this project more than three years. Several large media projects help us to test and debug the Editor, to make its core more stable. At the same time we significantly improved the API. Now, it can be used to create any plugin for any task. Hope you enjoy. 😏'\n            }\n          },\n          {\n            type: 'image',\n            data: {\n              url: 'assets/codex2x.png',\n              caption: '',\n              stretched: false,\n              withBorder: true,\n              withBackground: false,\n            }\n          },\n        ]\n      },\n      onReady: function(){\n        saveButton.click();\n      },\n      onChange: function(api, event) {\n        console.log('something changed', event);\n      }\n    });\n\n    /**\n     * Saving button\n     */\n    const saveButton = document.getElementById('saveButton');\n\n    /**\n     * Toggle read-only button\n     */\n    const toggleReadOnlyButton = document.getElementById('toggleReadOnlyButton');\n    const readOnlyIndicator = document.getElementById('readonly-state');\n\n    /**\n     * Saving example\n     */\n    saveButton.addEventListener('click', function () {\n      editor.save()\n        .then((savedData) => {\n          cPreview.show(savedData, document.getElementById(\"output\"));\n        })\n        .catch((error) => {\n          console.error('Saving error', error);\n        });\n    });\n\n    /**\n     * Toggle read-only example\n     */\n    toggleReadOnlyButton.addEventListener('click', async () => {\n      const readOnlyState = await editor.readOnly.toggle();\n\n      readOnlyIndicator.textContent = readOnlyState ? 'On' : 'Off';\n    });\n  </script>\n</body>\n</html>\n"
  },
  {
    "path": "index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <title>Editor.js 🤩🧦🤨 example</title>\n  <link href=\"https://fonts.googleapis.com/css?family=PT+Mono\" rel=\"stylesheet\">\n  <link href=\"/assets/demo.css\" rel=\"stylesheet\">\n  <script src=\"/assets/json-preview.js\"></script>\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\" />\n</head>\n<body>\n  <script>\n    if (localStorage.getItem('theme') === 'dark') {\n      document.body.classList.add(\"dark-mode\");\n    }\n  </script>\n  <div class=\"ce-example\">\n    <div class=\"ce-example__header\">\n      <a class=\"ce-example__header-logo\" href=\"https://codex.so/editor\">Editor.js 🤩🧦🤨</a>\n\n      <div class=\"ce-example__header-menu\">\n        <a href=\"https://github.com/editor-js\" target=\"_blank\">Plugins</a>\n        <a href=\"https://editorjs.io/usage\" target=\"_blank\">Usage</a>\n        <a href=\"https://editorjs.io/configuration\" target=\"_blank\">Configuration</a>\n        <a href=\"https://editorjs.io/creating-a-block-tool\" target=\"_blank\">API</a>\n      </div>\n    </div>\n    <div class=\"ce-example__content\">\n      <div id=\"editorjs\"></div>\n      <div class=\"ce-example__button\" id=\"saveButton\">\n        editor.save()\n      </div>\n      <div class=\"ce-example__statusbar\">\n        <div class=\"ce-example__statusbar-item\">\n          Readonly:\n          <b id=\"readonly-state\">\n            Off\n          </b>\n          &nbsp;\n          <div class=\"ce-example__statusbar-button\" id=\"toggleReadOnlyButton\">\n            toggle\n          </div>\n        </div>\n        <div class=\"ce-example__statusbar-item\">\n          <div class=\"ce-example__statusbar-button\" id=\"showBlocksBoundariesButton\">\n            <span data-toggled-text=\"Hide\">Show</span>\n            blocks boundaries\n          </div>\n        </div>\n        <div class=\"ce-example__statusbar-item\">\n          <div class=\"ce-example__statusbar-button\" id=\"enableThinModeButton\">\n            <span data-toggled-text=\"Disable\">Enable</span>\n            thin mode\n          </div>\n        </div>\n        <div class=\"ce-example__statusbar-item ce-example__statusbar-item--right\">\n          <div class=\"ce-example__statusbar-toggler\" id=\"darkThemeToggler\">\n          </div>\n        </div>\n      </div>\n    </div>\n    <div class=\"ce-example__output\">\n      <pre class=\"ce-example__output-content\" id=\"output\"></pre>\n\n      <div class=\"ce-example__output-footer\">\n        <a href=\"https://codex.so\" style=\"font-weight: bold;\">Made by CodeX</a>\n      </div>\n    </div>\n  </div>\n\n\n  <!-- Load Tools -->\n  <!--\n    You can upload Tools to your project's directory and connect them by relative links.\n\n    Also you can load each Tool from CDN or use NPM/Yarn packages.\n\n    Read more at Tools Connection doc:\n    https://editorjs.io/getting-started#tools-connection\n  -->\n  <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/header@latest\"></script><!-- Header -->\n  <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/simple-image@latest\"></script><!-- Image -->\n  <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/delimiter@latest\"></script><!-- Delimiter -->\n  <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/nested-list@latest\"></script><!-- List -->\n  <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/checklist@latest\"></script><!-- Checklist -->\n  <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/quote@latest\"></script><!-- Quote -->\n  <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/code@latest\"></script><!-- Code -->\n  <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/embed@latest\"></script><!-- Embed -->\n  <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/table@latest\"></script><!-- Table -->\n  <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/link@latest\"></script><!-- Link -->\n  <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/warning@latest\"></script><!-- Warning -->\n  <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/raw@latest\"></script><!-- Raw -->\n\n  <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/marker@latest\"></script><!-- Marker -->\n  <script src=\"https://cdn.jsdelivr.net/npm/@editorjs/inline-code@latest\"></script><!-- Inline Code -->\n\n  <!-- Initialization -->\n  <script type=\"module\">\n    import EditorJS from './src/codex.ts';\n\n    window.EditorJS = EditorJS;\n\n    /**\n     * To initialize the Editor, create a new instance with configuration object\n     * @see docs/installation.md for mode details\n     */\n    const editorConfig = {\n      /**\n       * Enable/Disable the read only mode\n       */\n      readOnly: false,\n\n      /**\n       * Wrapper of Editor\n       */\n      holder: 'editorjs',\n\n      /**\n       * Common Inline Toolbar settings\n       * - if true (or not specified), the order from 'tool' property will be used\n       * - if an array of tool names, this order will be used\n       */\n      // inlineToolbar: ['link', 'marker', 'bold', 'italic'],\n      // inlineToolbar: true,\n\n      /**\n       * Tools list\n       */\n      tools: {\n        /**\n         * Each Tool is a Plugin. Pass them via 'class' option with necessary settings {@link docs/tools.md}\n         */\n        header: {\n          class: Header,\n          inlineToolbar: ['link', 'marker'],\n          config: {\n            placeholder: 'Header'\n          },\n          shortcut: 'CMD+SHIFT+H'\n        },\n\n        /**\n         * Or pass class directly without any configuration\n         */\n        image: SimpleImage,\n\n        list: {\n          class: NestedList,\n          inlineToolbar: true,\n          shortcut: 'CMD+SHIFT+L'\n        },\n\n        checklist: {\n          class: Checklist,\n          inlineToolbar: true,\n        },\n\n        quote: {\n          class: Quote,\n          inlineToolbar: true,\n          config: {\n            quotePlaceholder: 'Enter a quote',\n            captionPlaceholder: 'Quote\\'s author',\n          },\n          shortcut: 'CMD+SHIFT+O'\n        },\n\n        warning: Warning,\n\n        marker: {\n          class:  Marker,\n          shortcut: 'CMD+SHIFT+M'\n        },\n\n        code: {\n          class:  CodeTool,\n          shortcut: 'CMD+SHIFT+C'\n        },\n\n        delimiter: Delimiter,\n\n        inlineCode: {\n          class: InlineCode,\n          shortcut: 'CMD+SHIFT+C'\n        },\n\n        linkTool: LinkTool,\n\n        raw: RawTool,\n\n        embed: Embed,\n\n        table: {\n          class: Table,\n          inlineToolbar: true,\n          shortcut: 'CMD+ALT+T'\n        },\n\n      },\n\n      /**\n       * This Tool will be used as default\n       */\n      // defaultBlock: 'paragraph',\n\n      placeholder: 'Write something or press / to select a tool',\n      autofocus: true,\n\n      /**\n       * Initial Editor data\n       */\n      data: {\n        blocks: [\n          {\n            id: \"zcKCF1S7X8\",\n            type: \"header\",\n            data: {\n              text: \"Editor.js\",\n              level: 1\n            }\n          },\n          {\n            id: \"b6ji-DvaKb\",\n            type: \"paragraph\",\n            data: {\n              text: \"Hey. Meet the new Editor. On this page you can see it in action — try to edit this text. Source code of the page contains the example of connection and configuration.\"\n            }\n          },\n          {\n            type: \"header\",\n            id: \"7ItVl5biRo\",\n            data: {\n              text: \"Key features\",\n              level: 2\n            }\n          },\n          {\n            type : 'list',\n            id: \"SSBSguGvP7\",\n            data : {\n              items : [\n                {\n                  content: 'It is a block-styled editor',\n                  items: []\n                },\n                {\n                  content: 'It returns clean data output in JSON',\n                  items: []\n                },\n                {\n                  content: 'Designed to be extendable and pluggable with a simple API',\n                  items: []\n                }\n              ],\n              style: 'unordered'\n            }\n          },\n          {\n            type: \"header\",\n            id: \"QZFox1m_ul\",\n            data: {\n              text: \"What does it mean «block-styled editor»\",\n              level: 2\n            }\n          },\n          {\n            type : 'paragraph',\n            id: \"bwnFX5LoX7\",\n            data : {\n              text : 'Workspace in classic editors is made of a single contenteditable element, used to create different HTML markups. Editor.js <mark class=\\\"cdx-marker\\\">workspace consists of separate Blocks: paragraphs, headings, images, lists, quotes, etc</mark>. Each of them is an independent contenteditable element (or more complex structure) provided by Plugin and united by Editor\\'s Core.'\n            }\n          },\n          {\n            type : 'paragraph',\n            id: \"mTrPOHAQTe\",\n            data : {\n              text : `There are dozens of <a href=\"https://github.com/editor-js\">ready-to-use Blocks</a> and the <a href=\"https://editorjs.io/creating-a-block-tool\">simple API</a> for creation any Block you need. For example, you can implement Blocks for Tweets, Instagram posts, surveys and polls, CTA-buttons and even games.`\n            }\n          },\n          {\n            type: \"header\",\n            id: \"1sYMhUrznu\",\n            data: {\n              text: \"What does it mean clean data output\",\n              level: 2\n            }\n          },\n          {\n            type : 'paragraph',\n            id: \"jpd7WEXrJG\",\n            data : {\n              text : 'Classic WYSIWYG-editors produce raw HTML-markup with both content data and content appearance. On the contrary, Editor.js outputs JSON object with data of each Block. You can see an example below'\n            }\n          },\n          {\n            type : 'paragraph',\n            id: \"0lOGNUKxqt\",\n            data : {\n              text : `Given data can be used as you want: render with HTML for <code class=\"inline-code\">Web clients</code>, render natively for <code class=\"inline-code\">mobile apps</code>, create markup for <code class=\"inline-code\">Facebook Instant Articles</code> or <code class=\"inline-code\">Google AMP</code>, generate an <code class=\"inline-code\">audio version</code> and so on.`\n            }\n          },\n          {\n            type : 'paragraph',\n            id: \"WvX7kBjp0I\",\n            data : {\n              text : 'Clean data is useful to sanitize, validate and process on the backend.'\n            }\n          },\n          {\n            type : 'delimiter',\n            id: \"H9LWKQ3NYd\",\n            data : {}\n          },\n          {\n            type : 'paragraph',\n            id: \"h298akk2Ad\",\n            data : {\n              text : 'We have been working on this project more than three years. Several large media projects help us to test and debug the Editor, to make its core more stable. At the same time we significantly improved the API. Now, it can be used to create any plugin for any task. Hope you enjoy. 😏'\n            }\n          },\n          {\n            type: 'image',\n            id: \"9802bjaAA2\",\n            data: {\n              url: '/assets/codex2x.png',\n              caption: '',\n              stretched: false,\n              withBorder: true,\n              withBackground: false,\n            }\n          },\n        ]\n      },\n      onReady: function(){\n        saveButton.click();\n      },\n      onChange: function(api, event) {\n        console.log('something changed', event);\n      },\n    }\n    /**\n    * To initialize the Editor, create a new instance with configuration object\n    * @see docs/installation.md for mode details\n    */\n    var editor = new EditorJS(editorConfig);\n\n    /**\n    * Saving button\n    */\n    const saveButton = document.getElementById('saveButton');\n\n    /**\n    * Toggle read-only button\n    */\n    const toggleReadOnlyButton = document.getElementById('toggleReadOnlyButton');\n    const readOnlyIndicator = document.getElementById('readonly-state');\n\n    /**\n    * Saving example\n    */\n    saveButton.addEventListener('click', function () {\n      editor.save()\n        .then((savedData) => {\n          cPreview.show(savedData, document.getElementById(\"output\"));\n        })\n        .catch((error) => {\n          console.error('Saving error', error);\n        });\n    });\n\n    /**\n    * Toggle read-only example\n    */\n    toggleReadOnlyButton.addEventListener('click', async () => {\n      const readOnlyState = await editor.readOnly.toggle();\n\n      readOnlyIndicator.textContent = readOnlyState ? 'On' : 'Off';\n    });\n\n    /**\n    * Button for displaying blocks borders. Useful for UI development\n    */\n    const showBlocksBoundariesButton = document.getElementById(\"showBlocksBoundariesButton\");\n\n    showBlocksBoundariesButton.addEventListener('click', () => {\n      document.body.classList.toggle(\"show-block-boundaries\")\n    })\n\n    /**\n    * Button for enabling the 'Thin' mode\n    */\n    const enableThinModeButton = document.getElementById(\"enableThinModeButton\");\n\n    enableThinModeButton.addEventListener('click', () => {\n      document.body.classList.toggle(\"thin-mode\")\n\n      editor.destroy();\n\n      editor = new EditorJS(editorConfig);\n    })\n\n    /**\n    * Toggler for toggling the dark mode\n    */\n    const darkThemeToggler = document.getElementById(\"darkThemeToggler\");\n\n    darkThemeToggler.addEventListener('click', () => {\n      document.body.classList.toggle(\"dark-mode\");\n\n      localStorage.setItem('theme', document.body.classList.contains(\"dark-mode\") ? 'dark' : 'default');\n    })\n\n    window.editor = editor;\n  </script>\n</body>\n</html>\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"@editorjs/editorjs\",\n  \"version\": \"2.31.5\",\n  \"description\": \"Editor.js — open source block-style WYSIWYG editor with JSON output\",\n  \"main\": \"dist/editorjs.umd.js\",\n  \"module\": \"dist/editorjs.mjs\",\n  \"types\": \"./types/index.d.ts\",\n  \"keywords\": [\n    \"codex editor\",\n    \"text editor\",\n    \"editor\",\n    \"editor.js\",\n    \"editorjs\",\n    \"wysiwyg\"\n  ],\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"vite build --mode production\",\n    \"build:test\": \"vite build --mode test\",\n    \"lint\": \"eslint src/ --ext .ts && yarn lint:tests\",\n    \"lint:errors\": \"eslint src/ --ext .ts --quiet\",\n    \"lint:fix\": \"eslint src/ --ext .ts --fix\",\n    \"lint:tests\": \"eslint test/ --ext .ts\",\n    \"test:e2e\": \"yarn build:test && cypress run\",\n    \"test:e2e:open\": \"yarn build:test && cypress open\"\n  },\n  \"author\": \"CodeX\",\n  \"license\": \"Apache-2.0\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/codex-team/editor.js.git\"\n  },\n  \"devDependencies\": {\n    \"@babel/register\": \"^7.21.0\",\n    \"@codexteam/icons\": \"0.3.2\",\n    \"@codexteam/shortcuts\": \"^1.1.1\",\n    \"@cypress/code-coverage\": \"^3.10.3\",\n    \"@editorjs/code\": \"^2.7.0\",\n    \"@editorjs/delimiter\": \"^1.2.0\",\n    \"@editorjs/header\": \"^2.8.8\",\n    \"@editorjs/paragraph\": \"^2.11.6\",\n    \"@editorjs/simple-image\": \"^1.4.1\",\n    \"@types/node\": \"^18.15.11\",\n    \"chai-subset\": \"^1.6.0\",\n    \"core-js\": \"3.30.0\",\n    \"cypress\": \"^13.13.3\",\n    \"cypress-intellij-reporter\": \"^0.0.7\",\n    \"cypress-plugin-tab\": \"^1.0.5\",\n    \"cypress-terminal-report\": \"^5.3.2\",\n    \"cypress-vite\": \"^1.5.0\",\n    \"eslint\": \"^8.37.0\",\n    \"eslint-config-codex\": \"^1.7.1\",\n    \"eslint-plugin-chai-friendly\": \"^0.7.2\",\n    \"eslint-plugin-cypress\": \"2.12.1\",\n    \"html-janitor\": \"^2.0.4\",\n    \"nanoid\": \"^4.0.2\",\n    \"postcss-apply\": \"^0.12.0\",\n    \"postcss-nested\": \"4.1.2\",\n    \"postcss-preset-env\": \"^8.3.0\",\n    \"rollup-plugin-license\": \"^3.0.1\",\n    \"stylelint\": \"^15.4.0\",\n    \"tslint\": \"^6.1.1\",\n    \"typescript\": \"5.0.3\",\n    \"vite\": \"^4.2.1\",\n    \"vite-plugin-css-injected-by-js\": \"^3.1.0\"\n  },\n  \"collective\": {\n    \"type\": \"opencollective\",\n    \"url\": \"https://opencollective.com/editorjs\"\n  },\n  \"dependencies\": {\n    \"@editorjs/caret\": \"^1.0.1\",\n    \"codex-notifier\": \"^1.1.2\",\n    \"codex-tooltip\": \"^1.0.5\"\n  }\n}\n"
  },
  {
    "path": "public/assets/demo.css",
    "content": "/**\n * Styles for the example page\n */\n\n:root {\n  --color-bg-main: #fff;\n  --color-border-light: #E8E8EB;\n  --color-text-main: #000;\n}\n\n.dark-mode {\n  --color-border-light: rgba(255, 255, 255,.08);\n  --color-bg-main: #1c1e24;\n  --color-text-main: #737886;\n}\n\n\nbody {\n  font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\", \"Ubuntu\", \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n  font-size: 14px;\n  line-height: 1.5em;\n  margin: 0;\n  background: var(--color-bg-main);\n  color: var(--color-text-main);\n}\n\n.ce-example {\n  font-size: 16.2px;\n}\n\n.ce-example__header {\n  border-bottom: 1px solid var(--color-border-light);\n  height: 50px;\n  line-height: 50px;\n  display: flex;\n  padding: 0 30px;\n  margin-bottom: 30px;\n  flex-wrap: wrap;\n}\n\n.ce-example__header a {\n  color: inherit;\n  text-decoration: none;\n}\n\n.ce-example__header-logo {\n  font-weight: bold;\n}\n\n.ce-example__header-menu {\n  margin-left: auto;\n}\n\n@media all and (max-width: 730px){\n  .ce-example__header-menu {\n    margin-left: 0;\n    margin-top: 10px;\n    flex-basis: 100%;\n    font-size: 14px;\n  }\n}\n\n.ce-example__header-menu a {\n  margin-left: 20px;\n}\n\n@media all and (max-width: 730px){\n  .ce-example__header-menu a {\n    margin-left: 0;\n    margin-right: 15px;\n  }\n}\n\n.ce-example__content {\n  max-width: 1100px;\n  margin: 0 auto;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\n.thin-mode .ce-example__content {\n  max-width: 500px;\n  border-left: 1px solid #eee;\n  border-right: 1px solid #eee;\n  padding: 0 15px;\n}\n\n.ce-example__output {\n  background: #1B202B;\n  overflow-x: auto;\n  padding: 0 30px 80px;\n}\n\n.ce-example__output-content {\n  max-width: 650px;\n  margin: 30px auto;\n  color: #ABADC3;\n  font-family: 'PT Mono', Menlo, Monaco, Consolas, Courier New, monospace;\n  font-size: 13.3px;\n}\n\n.ce-example__output-content:empty {\n  display: none;\n}\n\n.ce-example__button {\n  display: block;\n  margin: 50px auto;\n  max-width: 180px;\n  background: #4A9DF8;\n  padding: 17px 30px;\n  box-shadow: 0 22px 18px -4px rgba(137, 207, 255, 0.77);\n  transition: all 150ms ease;\n  cursor: pointer;\n  border-radius: 31px;\n  color: #fff;\n  font-family: 'PT Mono', Menlo, Monaco, Consolas, Courier New, monospace;\n  text-align: center;\n}\n\n.ce-example__button:hover {\n  background: #3D8DE5;\n  transform: translateY(2px);\n  box-shadow: 0 20px 15px -4px rgba(137, 207, 255, 0.77);\n}\n\n.ce-example__output-footer {\n  padding: 30px 0;\n  font-size: 14.2px;\n  letter-spacing: 0.3px;\n  text-align: center;\n}\n\n.ce-example__output-footer a {\n  color: #fff;\n  text-decoration: none;\n}\n\n.ce-example__statusbar {\n  display: flex;\n  align-items: center;\n  position: fixed;\n  bottom: 0;\n  right: 0;\n  left: 0;\n  background: var(--color-bg-main);\n  border-radius: 8px 8px 0 0;\n  border-top: 1px solid var(--color-border-light);\n  box-shadow: 0 2px 6px var(--color-border-light);\n  font-size: 13px;\n  padding: 8px 15px;\n  z-index: 1;\n  user-select: none;\n}\n\n@media (max-width: 768px) {\n  .ce-example__statusbar {\n    display: none;\n  }\n}\n\n.ce-example__statusbar-item:not(:last-of-type)::after {\n  content: '|';\n  color: #ddd;\n  margin: 0 15px 0 12px;\n}\n\n.ce-example__statusbar-item--right {\n  margin-left: auto;\n}\n\n.ce-example__statusbar-button {\n  display: inline-block;\n  padding: 3px 12px;\n  transition: all 150ms ease;\n  cursor: pointer;\n  border-radius: 31px;\n  background: #eff1f4;\n  text-align: center;\n  user-select: none;\n}\n\n.ce-example__statusbar-button:hover {\n  background: #e0e4eb;\n}\n\n.ce-example__statusbar-button-primary {\n  background: #4A9DF8;\n  color: #fff;\n  box-shadow: 0 7px 8px -4px rgba(137, 207, 255, 0.77);\n  font-family: 'PT Mono', Menlo, Monaco, Consolas, Courier New, monospace;\n}\n\n.ce-example__statusbar {\n  --toggler-size: 20px;\n}\n\n.ce-example__statusbar-toggler {\n  position: relative;\n  background: #7b8799;\n  border-radius: 20px;\n  padding: 2px;\n  width: calc(var(--toggler-size) * 2.2);\n  cursor: pointer;\n  user-select: none;\n}\n\n.ce-example__statusbar-toggler::before {\n  display: block;\n  content: '';\n  width: var(--toggler-size);\n  height: var(--toggler-size);\n  background: #fff;\n  border-radius: 50%;\n  transition: transform 100ms ease-in;\n}\n\n.ce-example__statusbar-toggler::after {\n  --moon-size: calc(var(--toggler-size) * 0.5);\n  content: '';\n  position: absolute;\n  top: 5px;\n  right: 5px;\n  height: var(--moon-size);\n  width: var(--moon-size);\n  box-shadow: calc(var(--moon-size) * 0.25 * -1) calc(var(--moon-size) * 0.18) 0 calc(var(--moon-size) * 0.05) white;\n  border-radius: 50%;\n}\n\n@media all and (max-width: 730px){\n  .ce-example__header,\n  .ce-example__content{\n    padding: 0 20px;\n  }\n}\n\n/**\n * JSON highlighter\n */\n.sc_attr {\n  color: rgb(148, 162, 192);\n}\n.sc_key {\n  color: rgb(190, 213, 255);\n}\n.sc_toolname {\n  color: rgb(15, 205, 251);\n}\n.sc_tag {\n  color: rgb(4, 131, 216);\n}\n.sc_bool {\n  color: rgb(247, 60, 173);\n}\n\n.ce-example .ce-block:first-of-type h1.ce-header{\n  font-size: 50px;\n}\n\n.ce-example-multiple {\n  display: grid;\n  grid-template-columns: calc(50% - 15px) calc(50% - 15px);\n  gap: 30px;\n  padding: 30px;\n}\n\n.ce-example-multiple > div {\n  background: #fff;\n  border-radius: 7px;\n  padding: 30px;\n}\n\n\n/**\n * Styles for the popup example page\n */\n.ce-example--popup {\n  height: 100vh;\n  display: flex;\n  flex-direction: column;\n}\n\n.ce-example--popup .ce-example__content {\n  flex-grow: 2;\n}\n\n.ce-example-popup__overlay {\n  position: fixed;\n  top: 0;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  background: #00000085;\n}\n\n.ce-example-popup__popup {\n  position: absolute;\n  left: 50%;\n  top: 50%;\n  transform: translate(-50%,-50%);\n  width: 800px;\n  max-width: 100%;\n  max-height: 90vh;\n  background: white;\n  padding: 20px;\n  border-radius: 8px;\n  overflow: auto;\n  box-sizing: border-box;\n}\n\n@media all and (max-width: 730px){\n  .ce-example-popup__popup {\n    top: 10px;\n    left: 10px;\n    width: calc(100% - 20px);\n    height: calc(100% - 20px);\n    transform: none;\n    max-height: none;\n\n  }\n}\n\n.show-block-boundaries .ce-block {\n  box-shadow: inset 0 0 0 1px #eff2f5;\n}\n\n.show-block-boundaries .ce-block__content {\n  box-shadow: 0 0 0 1px rgba(224, 231, 241, 0.61) inset;\n}\n.show-block-boundaries #showBlocksBoundariesButton span,\n.thin-mode #enableThinModeButton span {\n  font-size: 0;\n  vertical-align: bottom;\n}\n\n.show-block-boundaries #showBlocksBoundariesButton span::before,\n.thin-mode #enableThinModeButton span::before {\n  content: attr(data-toggled-text);\n  display: inline;\n  font-size: 13px;\n}\n\n\n\n/**\n * Dark theme overrides\n */\n.dark-mode img {\n  opacity: 0.5;\n}\n\n.dark-mode .cdx-simple-image__picture--with-border,\n.dark-mode .cdx-input {\n  border-color: var(--color-border-light);\n}\n\n.dark-mode .ce-example__button {\n  box-shadow: 0 24px 18px -14px rgba(4, 154, 255, 0.24);\n}\n\n.dark-mode .ce-example__output {\n  background-color: #17191f;\n}\n\n.dark-mode .inline-code {\n  background-color: rgba(53, 56, 68, 0.62);\n  color: #727683;\n}\n\n.dark-mode a {\n  color: #959ba8;\n}\n\n.dark-mode .ce-example__statusbar-toggler,\n.dark-mode .ce-example__statusbar-button {\n  background-color: #343842;\n}\n\n.dark-mode .ce-example__statusbar-toggler::before {\n  transform: translateX(calc(var(--toggler-size) * 2.2 - var(--toggler-size)));\n}\n\n.dark-mode .ce-example__statusbar-toggler::after {\n  content: '*';\n  right: auto;\n  left: 6px;\n  top: 7px;\n  color: #fff;\n  box-shadow: none;\n  font-size: 32px;\n}\n\n.dark-mode.show-block-boundaries .ce-block,\n.dark-mode.show-block-boundaries .ce-block__content {\n  box-shadow: 0 0 0 1px rgba(128, 144, 159, 0.09) inset;\n}\n\n.dark-mode.thin-mode .ce-example__content{\n  border-color: var(--color-border-light);\n}\n\n.dark-mode .ce-example__statusbar-item:not(:last-of-type)::after {\n  color: var(--color-border-light);\n}\n\n.dark-mode .ce-block--selected .ce-block__content,\n.dark-mode ::selection{\n  background-color: rgba(57, 68, 84, 0.57);\n}\n\n.dark-mode .ce-toolbox__button,\n.dark-mode .ce-toolbar__settings-btn,\n.dark-mode .ce-toolbar__plus {\n  color: inherit;\n}\n\n.dark-mode .ce-stub {\n  opacity: 0.3;\n}\n"
  },
  {
    "path": "public/assets/json-preview.js",
    "content": "/**\n * Module to compose output JSON preview\n */\nconst cPreview = (function (module) {\n  /**\n   * Shows JSON in pretty preview\n   * @param {object} output - what to show\n   * @param {Element} holder - where to show\n   */\n  module.show = function(output, holder) {\n    /** Make JSON pretty */\n    output = JSON.stringify( output, null, 4 );\n    /** Encode HTML entities */\n    output = encodeHTMLEntities( output );\n    /** Stylize! */\n    output = stylize( output );\n    holder.innerHTML = output;\n  };\n\n  /**\n   * Converts '>', '<', '&' symbols to entities\n   */\n  function encodeHTMLEntities(string) {\n    return string.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\n  }\n\n  /**\n   * Some styling magic\n   */\n  function stylize(string) {\n    /** Stylize JSON keys */\n    string = string.replace( /\"(\\w+)\"\\s?:/g, '\"<span class=sc_key>$1</span>\" :');\n    /** Stylize tool names */\n    string = string.replace( /\"(paragraph|quote|list|header|link|code|image|delimiter|raw|checklist|table|embed|warning)\"/g, '\"<span class=sc_toolname>$1</span>\"');\n    /** Stylize HTML tags */\n    string = string.replace( /(&lt;[\\/a-z]+(&gt;)?)/gi, '<span class=sc_tag>$1</span>' );\n    /** Stylize strings */\n    string = string.replace( /\"([^\"]+)\"/gi, '\"<span class=sc_attr>$1</span>\"' );\n    /** Boolean/Null */\n    string = string.replace( /\\b(true|false|null)\\b/gi, '<span class=sc_bool>$1</span>' );\n    return string;\n  }\n\n  return module;\n})({});\n"
  },
  {
    "path": "src/codex.ts",
    "content": "'use strict';\n\nimport type { EditorConfig } from '../types';\n\n/**\n * Apply polyfills\n */\nimport '@babel/register';\n\nimport './components/polyfills';\nimport Core from './components/core';\nimport * as _ from './components/utils';\nimport { destroy as destroyTooltip } from './components/utils/tooltip';\n\ndeclare const VERSION: string;\n\n/**\n * Editor.js\n *\n * @license Apache-2.0\n * @see Editor.js <https://editorjs.io>\n * @author CodeX Team <https://codex.so>\n */\nexport default class EditorJS {\n  /**\n   * Promise that resolves when core modules are ready and UI is rendered on the page\n   */\n  public isReady: Promise<void>;\n\n  /**\n   * Stores destroy method implementation.\n   * Clear heap occupied by Editor and remove UI components from the DOM.\n   */\n  public destroy: () => void;\n\n  /** Editor version */\n  public static get version(): string {\n    return VERSION;\n  }\n\n  /**\n   * @param {EditorConfig|string|undefined} [configuration] - user configuration\n   */\n  constructor(configuration?: EditorConfig|string) {\n    /**\n     * Set default onReady function\n     */\n    // eslint-disable-next-line @typescript-eslint/no-empty-function\n    let onReady = (): void => {};\n\n    /**\n     * If `onReady` was passed in `configuration` then redefine onReady function\n     */\n    if (_.isObject(configuration) && _.isFunction(configuration.onReady)) {\n      onReady = configuration.onReady;\n    }\n\n    /**\n     * Create a Editor.js instance\n     */\n    const editor = new Core(configuration);\n\n    /**\n     * We need to export isReady promise in the constructor\n     * as it can be used before other API methods are exported\n     *\n     * @type {Promise<void>}\n     */\n    this.isReady = editor.isReady.then(() => {\n      this.exportAPI(editor);\n      /**\n       * @todo pass API as an argument. It will allow to use Editor's API when editor is ready\n       */\n      onReady();\n    });\n  }\n\n  /**\n   * Export external API methods\n   *\n   * @param {Core} editor — Editor's instance\n   */\n  public exportAPI(editor: Core): void {\n    const fieldsToExport = [ 'configuration' ];\n    const destroy = (): void => {\n      Object.values(editor.moduleInstances)\n        .forEach((moduleInstance) => {\n          if (_.isFunction(moduleInstance.destroy)) {\n            moduleInstance.destroy();\n          }\n          moduleInstance.listeners.removeAll();\n        });\n\n      destroyTooltip();\n\n      editor = null;\n\n      for (const field in this) {\n        if (Object.prototype.hasOwnProperty.call(this, field)) {\n          delete this[field];\n        }\n      }\n\n      Object.setPrototypeOf(this, null);\n    };\n\n    fieldsToExport.forEach((field) => {\n      this[field] = editor[field];\n    });\n\n    this.destroy = destroy;\n\n    Object.setPrototypeOf(this, editor.moduleInstances.API.methods);\n\n    delete this.exportAPI;\n\n    const shorthands = {\n      blocks: {\n        clear: 'clear',\n        render: 'render',\n      },\n      caret: {\n        focus: 'focus',\n      },\n      events: {\n        on: 'on',\n        off: 'off',\n        emit: 'emit',\n      },\n      saver: {\n        save: 'save',\n      },\n    };\n\n    Object.entries(shorthands)\n      .forEach(([key, methods]) => {\n        Object.entries(methods)\n          .forEach(([name, alias]) => {\n            this[alias] = editor.moduleInstances.API.methods[key][name];\n          });\n      });\n  }\n}\n"
  },
  {
    "path": "src/components/__module.ts",
    "content": "import type { EditorModules } from '../types-internal/editor-modules';\nimport type { EditorConfig } from '../../types';\nimport type { ModuleConfig } from '../types-internal/module-config';\nimport Listeners from './utils/listeners';\nimport type EventsDispatcher from './utils/events';\nimport type { EditorEventMap } from './events';\n\n/**\n * The type <T> of the Module generic.\n * It describes the structure of nodes used in modules.\n */\nexport type ModuleNodes = object;\n\n/**\n * @abstract\n * @class      Module\n * @classdesc  All modules inherits from this class.\n * @typedef {Module} Module\n * @property {object} config - Editor user settings\n * @property {EditorModules} Editor - List of Editor modules\n */\nexport default class Module<T extends ModuleNodes = Record<string, HTMLElement>> {\n  /**\n   * Each module can provide some UI elements that will be stored in this property\n   */\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  public nodes: T = {} as any;\n\n  /**\n   * Editor modules list\n   *\n   * @type {EditorModules}\n   */\n  protected Editor: EditorModules;\n\n  /**\n   * Editor configuration object\n   *\n   * @type {EditorConfig}\n   */\n  protected config: EditorConfig;\n\n  /**\n   * Editor event dispatcher class\n   */\n  protected eventsDispatcher: EventsDispatcher<EditorEventMap>;\n\n  /**\n   * Util for bind/unbind DOM event listeners\n   */\n  protected listeners: Listeners = new Listeners();\n\n  /**\n   * This object provides methods to push into set of listeners that being dropped when read-only mode is enabled\n   */\n  protected readOnlyMutableListeners = {\n    /**\n     * Assigns event listener on DOM element and pushes into special array that might be removed\n     *\n     * @param {EventTarget} element - DOM Element\n     * @param {string} eventType - Event name\n     * @param {Function} handler - Event handler\n     * @param {boolean|AddEventListenerOptions} options - Listening options\n     */\n    on: (\n      element: EventTarget,\n      eventType: string,\n      handler: (event: Event) => void,\n      options: boolean | AddEventListenerOptions = false\n    ): void => {\n      this.mutableListenerIds.push(\n        this.listeners.on(element, eventType, handler, options)\n      );\n    },\n\n    /**\n     * Clears all mutable listeners\n     */\n    clearAll: (): void => {\n      for (const id of this.mutableListenerIds) {\n        this.listeners.offById(id);\n      }\n\n      this.mutableListenerIds = [];\n    },\n  };\n\n  /**\n   * The set of listener identifiers which will be dropped in read-only mode\n   */\n  private mutableListenerIds: string[] = [];\n\n  /**\n   * @class\n   * @param options - Module options\n   * @param options.config - Module config\n   * @param options.eventsDispatcher - Common event bus\n   */\n  constructor({ config, eventsDispatcher }: ModuleConfig) {\n    if (new.target === Module) {\n      throw new TypeError('Constructors for abstract class Module are not allowed.');\n    }\n\n    this.config = config;\n    this.eventsDispatcher = eventsDispatcher;\n  }\n\n  /**\n   * Editor modules setter\n   *\n   * @param {EditorModules} Editor - Editor's Modules\n   */\n  public set state(Editor: EditorModules) {\n    this.Editor = Editor;\n  }\n\n  /**\n   * Remove memorized nodes\n   */\n  public removeAllNodes(): void {\n    for (const key in this.nodes) {\n      const node = this.nodes[key];\n\n      if (node instanceof HTMLElement) {\n        node.remove();\n      }\n    }\n  }\n\n  /**\n   * Returns true if current direction is RTL (Right-To-Left)\n   */\n  protected get isRtl(): boolean {\n    return this.config.i18n.direction === 'rtl';\n  }\n}\n"
  },
  {
    "path": "src/components/block/api.ts",
    "content": "import type Block from './index';\nimport type { BlockToolData, ToolConfig, ToolboxConfigEntry } from '../../../types/tools';\nimport type { SavedData } from '../../../types/data-formats';\nimport type { BlockAPI as BlockAPIInterface } from '../../../types/api';\n\n/**\n * Constructs new BlockAPI object\n *\n * @class\n * @param {Block} block - Block to expose\n */\nfunction BlockAPI(\n  block: Block\n): void {\n  const blockAPI: BlockAPIInterface = {\n    /**\n     * Block id\n     *\n     * @returns {string}\n     */\n    get id(): string {\n      return block.id;\n    },\n    /**\n     * Tool name\n     *\n     * @returns {string}\n     */\n    get name(): string {\n      return block.name;\n    },\n\n    /**\n     * Tool config passed on Editor's initialization\n     *\n     * @returns {ToolConfig}\n     */\n    get config(): ToolConfig {\n      return block.config;\n    },\n\n    /**\n     * .ce-block element, that wraps plugin contents\n     *\n     * @returns {HTMLElement}\n     */\n    get holder(): HTMLElement {\n      return block.holder;\n    },\n\n    /**\n     * True if Block content is empty\n     *\n     * @returns {boolean}\n     */\n    get isEmpty(): boolean {\n      return block.isEmpty;\n    },\n\n    /**\n     * True if Block is selected with Cross-Block selection\n     *\n     * @returns {boolean}\n     */\n    get selected(): boolean {\n      return block.selected;\n    },\n\n    /**\n     * Set Block's stretch state\n     *\n     * @param {boolean} state — state to set\n     */\n    set stretched(state: boolean) {\n      block.stretched = state;\n    },\n\n    /**\n     * True if Block is stretched\n     *\n     * @returns {boolean}\n     */\n    get stretched(): boolean {\n      return block.stretched;\n    },\n\n    /**\n     * True if Block has inputs to be focused\n     */\n    get focusable(): boolean {\n      return block.focusable;\n    },\n\n    /**\n     * Call Tool method with errors handler under-the-hood\n     *\n     * @param {string} methodName - method to call\n     * @param {object} param - object with parameters\n     * @returns {unknown}\n     */\n    call(methodName: string, param?: object): unknown {\n      return block.call(methodName, param);\n    },\n\n    /**\n     * Save Block content\n     *\n     * @returns {Promise<void|SavedData>}\n     */\n    save(): Promise<void|SavedData> {\n      return block.save();\n    },\n\n    /**\n     * Validate Block data\n     *\n     * @param {BlockToolData} data - data to validate\n     * @returns {Promise<boolean>}\n     */\n    validate(data: BlockToolData): Promise<boolean> {\n      return block.validate(data);\n    },\n\n    /**\n     * Allows to say Editor that Block was changed. Used to manually trigger Editor's 'onChange' callback\n     * Can be useful for block changes invisible for editor core.\n     */\n    dispatchChange(): void {\n      block.dispatchChange();\n    },\n\n    /**\n     * Tool could specify several entries to be displayed at the Toolbox (for example, \"Heading 1\", \"Heading 2\", \"Heading 3\")\n     * This method returns the entry that is related to the Block (depended on the Block data)\n     */\n    getActiveToolboxEntry(): Promise<ToolboxConfigEntry | undefined> {\n      return block.getActiveToolboxEntry();\n    },\n  };\n\n  Object.setPrototypeOf(this, blockAPI);\n}\n\nexport default BlockAPI;\n"
  },
  {
    "path": "src/components/block/index.ts",
    "content": "import type {\n  BlockAPI as BlockAPIInterface,\n  BlockTool as IBlockTool,\n  BlockToolData,\n  BlockTune as IBlockTune,\n  SanitizerConfig,\n  ToolConfig,\n  ToolboxConfigEntry,\n  PopoverItemParams\n} from '../../../types';\n\nimport type { SavedData } from '../../../types/data-formats';\nimport $, { toggleEmptyMark } from '../dom';\nimport * as _ from '../utils';\nimport type ApiModules from '../modules/api';\nimport BlockAPI from './api';\nimport SelectionUtils from '../selection';\nimport type BlockToolAdapter from '../tools/block';\n\nimport type BlockTuneAdapter from '../tools/tune';\nimport type { BlockTuneData } from '../../../types/block-tunes/block-tune-data';\nimport type ToolsCollection from '../tools/collection';\nimport EventsDispatcher from '../utils/events';\nimport type { TunesMenuConfigItem } from '../../../types/tools';\nimport { isMutationBelongsToElement } from '../utils/mutations';\nimport type { EditorEventMap } from '../events';\nimport { FakeCursorAboutToBeToggled, FakeCursorHaveBeenSet, RedactorDomChanged } from '../events';\nimport type { RedactorDomChangedPayload } from '../events/RedactorDomChanged';\nimport { convertBlockDataToString, isSameBlockData } from '../utils/blocks';\nimport { PopoverItemType } from '@/types/utils/popover/popover-item-type';\n\n/**\n * Interface describes Block class constructor argument\n */\ninterface BlockConstructorOptions {\n  /**\n   * Block's id. Should be passed for existed block, and omitted for a new one.\n   */\n  id?: string;\n\n  /**\n   * Initial Block data\n   */\n  data: BlockToolData;\n\n  /**\n   * Tool object\n   */\n  tool: BlockToolAdapter;\n\n  /**\n   * Editor's API methods\n   */\n  api: ApiModules;\n\n  /**\n   * This flag indicates that the Block should be constructed in the read-only mode.\n   */\n  readOnly: boolean;\n\n  /**\n   * Tunes data for current Block\n   */\n  tunesData: { [name: string]: BlockTuneData };\n}\n\n/**\n * @class Block\n * @classdesc This class describes editor`s block, including block`s HTMLElement, data and tool\n * @property {BlockTool} tool — current block tool (Paragraph, for example)\n * @property {object} CSS — block`s css classes\n */\n\n/**\n * Available Block Tool API methods\n */\nexport enum BlockToolAPI {\n  /**\n   * @todo remove method in 3.0.0\n   * @deprecated — use 'rendered' hook instead\n   */\n  // eslint-disable-next-line @typescript-eslint/naming-convention\n  APPEND_CALLBACK = 'appendCallback',\n  RENDERED = 'rendered',\n  MOVED = 'moved',\n  UPDATED = 'updated',\n  REMOVED = 'removed',\n  // eslint-disable-next-line @typescript-eslint/naming-convention\n  ON_PASTE = 'onPaste',\n}\n\n/**\n * Names of events used in Block\n */\ninterface BlockEvents {\n  'didMutated': Block,\n}\n\n/**\n * @classdesc Abstract Block class that contains Block information, Tool name and Tool class instance\n * @property {BlockTool} tool - Tool instance\n * @property {HTMLElement} holder - Div element that wraps block content with Tool's content. Has `ce-block` CSS class\n * @property {HTMLElement} pluginsContent - HTML content that returns by Tool's render function\n */\nexport default class Block extends EventsDispatcher<BlockEvents> {\n  /**\n   * CSS classes for the Block\n   *\n   * @returns {{wrapper: string, content: string}}\n   */\n  public static get CSS(): { [name: string]: string } {\n    return {\n      wrapper: 'ce-block',\n      wrapperStretched: 'ce-block--stretched',\n      content: 'ce-block__content',\n      selected: 'ce-block--selected',\n      dropTarget: 'ce-block--drop-target',\n    };\n  }\n\n  /**\n   * Block unique identifier\n   */\n  public id: string;\n\n  /**\n   * Block Tool`s name\n   */\n  public readonly name: string;\n\n  /**\n   * Instance of the Tool Block represents\n   */\n  public readonly tool: BlockToolAdapter;\n\n  /**\n   * User Tool configuration\n   */\n  public readonly settings: ToolConfig;\n\n  /**\n   * Wrapper for Block`s content\n   */\n  public readonly holder: HTMLDivElement;\n\n  /**\n   * Tunes used by Tool\n   */\n  public readonly tunes: ToolsCollection<BlockTuneAdapter>;\n\n  /**\n   * Tool's user configuration\n   */\n  public readonly config: ToolConfig;\n\n  /**\n   * Cached inputs\n   */\n  private cachedInputs: HTMLElement[] = [];\n\n  /**\n   * We'll store a reference to the tool's rendered element to access it later\n   */\n  private toolRenderedElement: HTMLElement | null = null;\n\n  /**\n   * Tool class instance\n   */\n  private readonly toolInstance: IBlockTool;\n\n  /**\n   * User provided Block Tunes instances\n   */\n  private readonly tunesInstances: Map<string, IBlockTune> = new Map();\n\n  /**\n   * Editor provided Block Tunes instances\n   */\n  private readonly defaultTunesInstances: Map<string, IBlockTune> = new Map();\n\n  /**\n   * If there is saved data for Tune which is not available at the moment,\n   * we will store it here and provide back on save so data is not lost\n   */\n  private unavailableTunesData: { [name: string]: BlockTuneData } = {};\n\n  /**\n   * Focused input index\n   *\n   * @type {number}\n   */\n  private inputIndex = 0;\n\n  /**\n   * Common editor event bus\n   */\n  private readonly editorEventBus: EventsDispatcher<EditorEventMap> | null = null;\n\n  /**\n   * Link to editor dom change callback. Used to remove listener on remove\n   */\n  private redactorDomChangedCallback: (payload: RedactorDomChangedPayload) => void;\n\n  /**\n   * Current block API interface\n   */\n  private readonly blockAPI: BlockAPIInterface;\n\n  /**\n   * @param options - block constructor options\n   * @param [options.id] - block's id. Will be generated if omitted.\n   * @param options.data - Tool's initial data\n   * @param options.tool — block's tool\n   * @param options.api - Editor API module for pass it to the Block Tunes\n   * @param options.readOnly - Read-Only flag\n   * @param [eventBus] - Editor common event bus. Allows to subscribe on some Editor events. Could be omitted when \"virtual\" Block is created. See BlocksAPI@composeBlockData.\n   */\n  constructor({\n    id = _.generateBlockId(),\n    data,\n    tool,\n    readOnly,\n    tunesData,\n  }: BlockConstructorOptions, eventBus?: EventsDispatcher<EditorEventMap>) {\n    super();\n    this.name = tool.name;\n    this.id = id;\n    this.settings = tool.settings;\n    this.config = tool.settings.config || {};\n    this.editorEventBus = eventBus || null;\n    this.blockAPI = new BlockAPI(this);\n\n    this.tool = tool;\n    this.toolInstance = tool.create(data, this.blockAPI, readOnly);\n\n    /**\n     * @type {BlockTuneAdapter[]}\n     */\n    this.tunes = tool.tunes;\n\n    this.composeTunes(tunesData);\n\n    this.holder = this.compose();\n\n    /**\n     * Bind block events in RIC for optimizing of constructing process time\n     */\n    window.requestIdleCallback(() => {\n      /**\n       * Start watching block mutations\n       */\n      this.watchBlockMutations();\n\n      /**\n       * Mutation observer doesn't track changes in \"<input>\" and \"<textarea>\"\n       * so we need to track focus events to update current input and clear cache.\n       */\n      this.addInputEvents();\n\n      /**\n       * We mark inputs with [data-empty] attribute\n       * It can be useful for developers, for example for correct placeholder behavior\n       */\n      this.toggleInputsEmptyMark();\n    });\n  }\n\n  /**\n   * Find and return all editable elements (contenteditable and native inputs) in the Tool HTML\n   */\n  public get inputs(): HTMLElement[] {\n    /**\n     * Return from cache if existed\n     */\n    if (this.cachedInputs.length !== 0) {\n      return this.cachedInputs;\n    }\n\n    const inputs = $.findAllInputs(this.holder);\n\n    /**\n     * If inputs amount was changed we need to check if input index is bigger then inputs array length\n     */\n    if (this.inputIndex > inputs.length - 1) {\n      this.inputIndex = inputs.length - 1;\n    }\n\n    /**\n     * Cache inputs\n     */\n    this.cachedInputs = inputs;\n\n    return inputs;\n  }\n\n  /**\n   * Return current Tool`s input\n   * If Block doesn't contain inputs, return undefined\n   */\n  public get currentInput(): HTMLElement | undefined {\n    return this.inputs[this.inputIndex];\n  }\n\n  /**\n   * Set input index to the passed element\n   *\n   * @param element - HTML Element to set as current input\n   */\n  public set currentInput(element: HTMLElement) {\n    const index = this.inputs.findIndex((input) => input === element || input.contains(element));\n\n    if (index !== -1) {\n      this.inputIndex = index;\n    }\n  }\n\n  /**\n   * Return first Tool`s input\n   * If Block doesn't contain inputs, return undefined\n   */\n  public get firstInput(): HTMLElement | undefined {\n    return this.inputs[0];\n  }\n\n  /**\n   * Return first Tool`s input\n   * If Block doesn't contain inputs, return undefined\n   */\n  public get lastInput(): HTMLElement | undefined {\n    const inputs = this.inputs;\n\n    return inputs[inputs.length - 1];\n  }\n\n  /**\n   * Return next Tool`s input or undefined if it doesn't exist\n   * If Block doesn't contain inputs, return undefined\n   */\n  public get nextInput(): HTMLElement | undefined {\n    return this.inputs[this.inputIndex + 1];\n  }\n\n  /**\n   * Return previous Tool`s input or undefined if it doesn't exist\n   * If Block doesn't contain inputs, return undefined\n   */\n  public get previousInput(): HTMLElement | undefined {\n    return this.inputs[this.inputIndex - 1];\n  }\n\n  /**\n   * Get Block's JSON data\n   *\n   * @returns {object}\n   */\n  public get data(): Promise<BlockToolData> {\n    return this.save().then((savedObject) => {\n      if (savedObject && !_.isEmpty(savedObject.data)) {\n        return savedObject.data;\n      } else {\n        return {};\n      }\n    });\n  }\n\n  /**\n   * Returns tool's sanitizer config\n   *\n   * @returns {object}\n   */\n  public get sanitize(): SanitizerConfig {\n    return this.tool.sanitizeConfig;\n  }\n\n  /**\n   * is block mergeable\n   * We plugin have merge function then we call it mergeable\n   *\n   * @returns {boolean}\n   */\n  public get mergeable(): boolean {\n    return _.isFunction(this.toolInstance.merge);\n  }\n\n  /**\n   * If Block contains inputs, it is focusable\n   */\n  public get focusable(): boolean {\n    return this.inputs.length !== 0;\n  }\n\n  /**\n   * Check block for emptiness\n   *\n   * @returns {boolean}\n   */\n  public get isEmpty(): boolean {\n    const emptyText = $.isEmpty(this.pluginsContent, '/');\n    const emptyMedia = !this.hasMedia;\n\n    return emptyText && emptyMedia;\n  }\n\n  /**\n   * Check if block has a media content such as images, iframe and other\n   *\n   * @returns {boolean}\n   */\n  public get hasMedia(): boolean {\n    /**\n     * This tags represents media-content\n     *\n     * @type {string[]}\n     */\n    const mediaTags = [\n      'img',\n      'iframe',\n      'video',\n      'audio',\n      'source',\n      'input',\n      'textarea',\n      'twitterwidget',\n    ];\n\n    return !!this.holder.querySelector(mediaTags.join(','));\n  }\n\n  /**\n   * Set selected state\n   * We don't need to mark Block as Selected when it is empty\n   *\n   * @param {boolean} state - 'true' to select, 'false' to remove selection\n   */\n  public set selected(state: boolean) {\n    this.holder.classList.toggle(Block.CSS.selected, state);\n\n    const fakeCursorWillBeAdded = state === true && SelectionUtils.isRangeInsideContainer(this.holder);\n    const fakeCursorWillBeRemoved = state === false && SelectionUtils.isFakeCursorInsideContainer(this.holder);\n\n    if (fakeCursorWillBeAdded || fakeCursorWillBeRemoved) {\n      this.editorEventBus?.emit(FakeCursorAboutToBeToggled, { state }); // mutex\n\n      if (fakeCursorWillBeAdded) {\n        SelectionUtils.addFakeCursor();\n      } else {\n        SelectionUtils.removeFakeCursor(this.holder);\n      }\n\n      this.editorEventBus?.emit(FakeCursorHaveBeenSet, { state });\n    }\n  }\n\n  /**\n   * Returns True if it is Selected\n   *\n   * @returns {boolean}\n   */\n  public get selected(): boolean {\n    return this.holder.classList.contains(Block.CSS.selected);\n  }\n\n  /**\n   * Set stretched state\n   *\n   * @param {boolean} state - 'true' to enable, 'false' to disable stretched state\n   */\n  public set stretched(state: boolean) {\n    this.holder.classList.toggle(Block.CSS.wrapperStretched, state);\n  }\n\n  /**\n   * Return Block's stretched state\n   *\n   * @returns {boolean}\n   */\n  public get stretched(): boolean {\n    return this.holder.classList.contains(Block.CSS.wrapperStretched);\n  }\n\n  /**\n   * Toggle drop target state\n   *\n   * @param {boolean} state - 'true' if block is drop target, false otherwise\n   */\n  public set dropTarget(state) {\n    this.holder.classList.toggle(Block.CSS.dropTarget, state);\n  }\n\n  /**\n   * Returns Plugins content\n   *\n   * @returns {HTMLElement}\n   */\n  public get pluginsContent(): HTMLElement {\n    return this.toolRenderedElement;\n  }\n\n  /**\n   * Calls Tool's method\n   *\n   * Method checks tool property {MethodName}. Fires method with passes params If it is instance of Function\n   *\n   * @param {string} methodName - method to call\n   * @param {object} params - method argument\n   */\n  public call(methodName: string, params?: object): void {\n    /**\n     * call Tool's method with the instance context\n     */\n    if (_.isFunction(this.toolInstance[methodName])) {\n      if (methodName === BlockToolAPI.APPEND_CALLBACK) {\n        _.log(\n          '`appendCallback` hook is deprecated and will be removed in the next major release. ' +\n          'Use `rendered` hook instead',\n          'warn'\n        );\n      }\n\n      try {\n        // eslint-disable-next-line no-useless-call\n        this.toolInstance[methodName].call(this.toolInstance, params);\n      } catch (e) {\n        _.log(`Error during '${methodName}' call: ${e.message}`, 'error');\n      }\n    }\n  }\n\n  /**\n   * Call plugins merge method\n   *\n   * @param {BlockToolData} data - data to merge\n   */\n  public async mergeWith(data: BlockToolData): Promise<void> {\n    await this.toolInstance.merge(data);\n  }\n\n  /**\n   * Extracts data from Block\n   * Groups Tool's save processing time\n   *\n   * @returns {object}\n   */\n  public async save(): Promise<undefined | SavedData> {\n    const extractedBlock = await this.toolInstance.save(this.pluginsContent as HTMLElement);\n    const tunesData: { [name: string]: BlockTuneData } = this.unavailableTunesData;\n\n    [\n      ...this.tunesInstances.entries(),\n      ...this.defaultTunesInstances.entries(),\n    ]\n      .forEach(([name, tune]) => {\n        if (_.isFunction(tune.save)) {\n          try {\n            tunesData[name] = tune.save();\n          } catch (e) {\n            _.log(`Tune ${tune.constructor.name} save method throws an Error %o`, 'warn', e);\n          }\n        }\n      });\n\n    /**\n     * Measuring execution time\n     */\n    const measuringStart = window.performance.now();\n    let measuringEnd;\n\n    return Promise.resolve(extractedBlock)\n      .then((finishedExtraction) => {\n        /** measure promise execution */\n        measuringEnd = window.performance.now();\n\n        return {\n          id: this.id,\n          tool: this.name,\n          data: finishedExtraction,\n          tunes: tunesData,\n          time: measuringEnd - measuringStart,\n        };\n      })\n      .catch((error) => {\n        _.log(`Saving process for ${this.name} tool failed due to the ${error}`, 'log', 'red');\n      });\n  }\n\n  /**\n   * Uses Tool's validation method to check the correctness of output data\n   * Tool's validation method is optional\n   *\n   * @description Method returns true|false whether data passed the validation or not\n   * @param {BlockToolData} data - data to validate\n   * @returns {Promise<boolean>} valid\n   */\n  public async validate(data: BlockToolData): Promise<boolean> {\n    let isValid = true;\n\n    if (this.toolInstance.validate instanceof Function) {\n      isValid = await this.toolInstance.validate(data);\n    }\n\n    return isValid;\n  }\n\n  /**\n   * Returns data to render in Block Tunes menu.\n   * Splits block tunes into 2 groups: block specific tunes and common tunes\n   */\n  public getTunes(): {\n    toolTunes: PopoverItemParams[];\n    commonTunes: PopoverItemParams[];\n    } {\n    const toolTunesPopoverParams: TunesMenuConfigItem[] = [];\n    const commonTunesPopoverParams: TunesMenuConfigItem[] = [];\n\n    /** Tool's tunes: may be defined as return value of optional renderSettings method */\n    const tunesDefinedInTool = typeof this.toolInstance.renderSettings === 'function' ? this.toolInstance.renderSettings() : [];\n\n    if ($.isElement(tunesDefinedInTool)) {\n      toolTunesPopoverParams.push({\n        type: PopoverItemType.Html,\n        element: tunesDefinedInTool,\n      });\n    } else if (Array.isArray(tunesDefinedInTool)) {\n      toolTunesPopoverParams.push(...tunesDefinedInTool);\n    } else {\n      toolTunesPopoverParams.push(tunesDefinedInTool);\n    }\n\n    /** Common tunes: combination of default tunes (move up, move down, delete) and third-party tunes connected via tunes api */\n    const commonTunes = [\n      ...this.tunesInstances.values(),\n      ...this.defaultTunesInstances.values(),\n    ].map(tuneInstance => tuneInstance.render());\n\n    /** Separate custom html from Popover items params for common tunes */\n    commonTunes.forEach(tuneConfig => {\n      if ($.isElement(tuneConfig)) {\n        commonTunesPopoverParams.push({\n          type: PopoverItemType.Html,\n          element: tuneConfig,\n        });\n      } else if (Array.isArray(tuneConfig)) {\n        commonTunesPopoverParams.push(...tuneConfig);\n      } else {\n        commonTunesPopoverParams.push(tuneConfig);\n      }\n    });\n\n    return {\n      toolTunes: toolTunesPopoverParams,\n      commonTunes: commonTunesPopoverParams,\n    };\n  }\n\n  /**\n   * Update current input index with selection anchor node\n   */\n  public updateCurrentInput(): void {\n    /**\n     * If activeElement is native input, anchorNode points to its parent.\n     * So if it is native input use it instead of anchorNode\n     *\n     * If anchorNode is undefined, also use activeElement\n     */\n    this.currentInput = $.isNativeInput(document.activeElement) || !SelectionUtils.anchorNode\n      ? document.activeElement\n      : SelectionUtils.anchorNode;\n  }\n\n  /**\n   * Allows to say Editor that Block was changed. Used to manually trigger Editor's 'onChange' callback\n   * Can be useful for block changes invisible for editor core.\n   */\n  public dispatchChange(): void {\n    this.didMutated();\n  }\n\n  /**\n   * Call Tool instance destroy method\n   */\n  public destroy(): void {\n    this.unwatchBlockMutations();\n    this.removeInputEvents();\n\n    super.destroy();\n\n    if (_.isFunction(this.toolInstance.destroy)) {\n      this.toolInstance.destroy();\n    }\n  }\n\n  /**\n   * Tool could specify several entries to be displayed at the Toolbox (for example, \"Heading 1\", \"Heading 2\", \"Heading 3\")\n   * This method returns the entry that is related to the Block (depended on the Block data)\n   */\n  public async getActiveToolboxEntry(): Promise<ToolboxConfigEntry | undefined> {\n    const toolboxSettings = this.tool.toolbox;\n\n    /**\n     * If Tool specifies just the single entry, treat it like an active\n     */\n    if (toolboxSettings.length === 1) {\n      return Promise.resolve(this.tool.toolbox[0]);\n    }\n\n    /**\n     * If we have several entries with their own data overrides,\n     * find those who matches some current data property\n     *\n     * Example:\n     *  Tools' toolbox: [\n     *    {title: \"Heading 1\", data: {level: 1} },\n     *    {title: \"Heading 2\", data: {level: 2} }\n     *  ]\n     *\n     *  the Block data: {\n     *    text: \"Heading text\",\n     *    level: 2\n     *  }\n     *\n     *  that means that for the current block, the second toolbox item (matched by \"{level: 2}\") is active\n     */\n    const blockData = await this.data;\n    const toolboxItems = toolboxSettings;\n\n    return toolboxItems?.find((item) => {\n      return isSameBlockData(item.data, blockData);\n    });\n  }\n\n  /**\n   * Exports Block data as string using conversion config\n   */\n  public async exportDataAsString(): Promise<string> {\n    const blockData = await this.data;\n\n    return convertBlockDataToString(blockData, this.tool.conversionConfig);\n  }\n\n  /**\n   * Make default Block wrappers and put Tool`s content there\n   *\n   * @returns {HTMLDivElement}\n   */\n  private compose(): HTMLDivElement {\n    const wrapper = $.make('div', Block.CSS.wrapper) as HTMLDivElement,\n        contentNode = $.make('div', Block.CSS.content),\n        pluginsContent = this.toolInstance.render();\n\n    if (import.meta.env.MODE === 'test') {\n      wrapper.setAttribute('data-cy', 'block-wrapper');\n    }\n\n    /**\n     * Export id to the DOM three\n     * Useful for standalone modules development. For example, allows to identify Block by some child node. Or scroll to a particular Block by id.\n     */\n    wrapper.dataset.id = this.id;\n\n    /**\n     * Saving a reference to plugin's content element for guaranteed accessing it later\n     */\n    this.toolRenderedElement = pluginsContent;\n\n    contentNode.appendChild(this.toolRenderedElement);\n\n    /**\n     * Block Tunes might wrap Block's content node to provide any UI changes\n     *\n     * <tune2wrapper>\n     *   <tune1wrapper>\n     *     <blockContent />\n     *   </tune1wrapper>\n     * </tune2wrapper>\n     */\n    let wrappedContentNode: HTMLElement = contentNode;\n\n    [...this.tunesInstances.values(), ...this.defaultTunesInstances.values()]\n      .forEach((tune) => {\n        if (_.isFunction(tune.wrap)) {\n          try {\n            wrappedContentNode = tune.wrap(wrappedContentNode);\n          } catch (e) {\n            _.log(`Tune ${tune.constructor.name} wrap method throws an Error %o`, 'warn', e);\n          }\n        }\n      });\n\n    wrapper.appendChild(wrappedContentNode);\n\n    return wrapper;\n  }\n\n  /**\n   * Instantiate Block Tunes\n   *\n   * @param tunesData - current Block tunes data\n   * @private\n   */\n  private composeTunes(tunesData: { [name: string]: BlockTuneData }): void {\n    Array.from(this.tunes.values()).forEach((tune) => {\n      const collection = tune.isInternal ? this.defaultTunesInstances : this.tunesInstances;\n\n      collection.set(tune.name, tune.create(tunesData[tune.name], this.blockAPI));\n    });\n\n    /**\n     * Check if there is some data for not available tunes\n     */\n    Object.entries(tunesData).forEach(([name, data]) => {\n      if (!this.tunesInstances.has(name)) {\n        this.unavailableTunesData[name] = data;\n      }\n    });\n  }\n\n  /**\n   * Is fired when text input or contentEditable is focused\n   */\n  private handleFocus = (): void => {\n    /**\n     * Drop inputs cache to query the new ones\n     */\n    this.dropInputsCache();\n\n    /**\n     * Update current input\n     */\n    this.updateCurrentInput();\n  };\n\n  /**\n   * Adds focus event listeners to all inputs and contenteditable\n   */\n  private addInputEvents(): void {\n    this.inputs.forEach(input => {\n      input.addEventListener('focus', this.handleFocus);\n\n      /**\n       * If input is native input add oninput listener to observe changes\n       */\n      if ($.isNativeInput(input)) {\n        input.addEventListener('input', this.didMutated as EventListener);\n      }\n    });\n  }\n\n  /**\n   * removes focus event listeners from all inputs and contenteditable\n   */\n  private removeInputEvents(): void {\n    this.inputs.forEach(input => {\n      input.removeEventListener('focus', this.handleFocus);\n\n      if ($.isNativeInput(input)) {\n        input.removeEventListener('input', this.didMutated as EventListener);\n      }\n    });\n  }\n\n  /**\n   * Is fired when DOM mutation has been happened\n   *\n   * @param mutationsOrInputEvent - actual changes\n   *   - MutationRecord[] - any DOM change\n   *   - InputEvent — <input> change\n   *   - undefined — manual triggering of block.dispatchChange()\n   */\n  private readonly didMutated = (mutationsOrInputEvent: MutationRecord[] | InputEvent = undefined): void => {\n    /**\n     * Block API have dispatchChange() method. In this case, mutations list will be undefined.\n     */\n    const isManuallyDispatched = mutationsOrInputEvent === undefined;\n\n    /**\n     * True if didMutated has been called as \"input\" event handler\n     */\n    const isInputEventHandler = mutationsOrInputEvent instanceof InputEvent;\n\n    /**\n     * If tool updates its own root element, we need to renew it in our memory\n     */\n    if (!isManuallyDispatched && !isInputEventHandler) {\n      this.detectToolRootChange(mutationsOrInputEvent);\n    }\n\n    /**\n     * We won't fire a Block mutation event if mutation contain only nodes marked with 'data-mutation-free' attributes\n     */\n    let shouldFireUpdate;\n\n    if (isManuallyDispatched) {\n      shouldFireUpdate = true;\n    } else if (isInputEventHandler) {\n      shouldFireUpdate = true;\n    } else {\n      /**\n       * Update from 2023, Feb 17:\n       *    Changed mutationsOrInputEvent.some() to mutationsOrInputEvent.every()\n       *    since there could be a real mutations same-time with mutation-free changes,\n       *    for example when Block Tune change: block is changing along with FakeCursor (mutation-free) removing\n       *    — we should fire 'didMutated' event in that case\n       */\n      const everyRecordIsMutationFree = mutationsOrInputEvent.length > 0 && mutationsOrInputEvent.every((record) => {\n        const { addedNodes, removedNodes, target } = record;\n        const changedNodes = [\n          ...Array.from(addedNodes),\n          ...Array.from(removedNodes),\n          target,\n        ];\n\n        return changedNodes.some((node) => {\n          if (!$.isElement(node)) {\n            /**\n             * \"characterData\" mutation record has Text node as a target, so we need to get parent element to check it for mutation-free attribute\n             */\n            node = node.parentElement;\n          }\n\n          return node && (node as HTMLElement).closest('[data-mutation-free=\"true\"]') !== null;\n        });\n      });\n\n      shouldFireUpdate = !everyRecordIsMutationFree;\n    }\n\n    /**\n     * In case some mutation free elements are added or removed, do not trigger didMutated event\n     */\n    if (!shouldFireUpdate) {\n      return;\n    }\n\n    this.dropInputsCache();\n\n    /**\n     * Update current input\n     */\n    this.updateCurrentInput();\n\n    /**\n     * We mark inputs with 'data-empty' attribute, so new inputs should be marked as well\n     */\n    this.toggleInputsEmptyMark();\n\n    this.call(BlockToolAPI.UPDATED);\n\n    /**\n     * Emit a Block Event with current Block instance.\n     * Block Manager subscribed to these events\n     */\n    this.emit('didMutated', this);\n  };\n\n  /**\n   * Listen common editor Dom Changed event and detect mutations related to the  Block\n   */\n  private watchBlockMutations(): void {\n    /**\n     * Save callback to a property to remove it on Block destroy\n     *\n     * @param payload - event payload\n     */\n    this.redactorDomChangedCallback = (payload) => {\n      const { mutations } = payload;\n\n      const mutationBelongsToBlock = mutations.some(record => isMutationBelongsToElement(record, this.toolRenderedElement));\n\n      if (mutationBelongsToBlock) {\n        this.didMutated(mutations);\n      }\n    };\n\n    this.editorEventBus?.on(RedactorDomChanged, this.redactorDomChangedCallback);\n  }\n\n  /**\n   * Remove redactor dom change event listener\n   */\n  private unwatchBlockMutations(): void {\n    this.editorEventBus?.off(RedactorDomChanged, this.redactorDomChangedCallback);\n  }\n\n  /**\n   * Sometimes Tool can replace own main element, for example H2 -> H4 or UL -> OL\n   * We need to detect such changes and update a link to tools main element with the new one\n   *\n   * @param mutations - records of block content mutations\n   */\n  private detectToolRootChange(mutations: MutationRecord[]): void {\n    mutations.forEach(record => {\n      const toolRootHasBeenUpdated = Array.from(record.removedNodes).includes(this.toolRenderedElement);\n\n      if (toolRootHasBeenUpdated) {\n        const newToolElement = record.addedNodes[record.addedNodes.length - 1];\n\n        this.toolRenderedElement = newToolElement as HTMLElement;\n      }\n    });\n  }\n\n  /**\n   * Clears inputs cached value\n   */\n  private dropInputsCache(): void {\n    this.cachedInputs = [];\n  }\n\n  /**\n   * Mark inputs with 'data-empty' attribute with the empty state\n   */\n  private toggleInputsEmptyMark(): void {\n    this.inputs.forEach(toggleEmptyMark);\n  }\n}\n"
  },
  {
    "path": "src/components/block-tunes/block-tune-delete.ts",
    "content": "/**\n * @class DeleteTune\n * @classdesc Editor's default tune that moves up selected block\n * @copyright <CodeX Team> 2018\n */\nimport type { API, BlockTune } from '../../../types';\nimport { IconCross } from '@codexteam/icons';\nimport type { MenuConfig } from '../../../types/tools/menu-config';\n\n/**\n *\n */\nexport default class DeleteTune implements BlockTune {\n  /**\n   * Set Tool is Tune\n   */\n  public static readonly isTune = true;\n\n  /**\n   * Property that contains Editor.js API methods\n   *\n   * @see {@link docs/api.md}\n   */\n  private readonly api: API;\n\n  /**\n   * DeleteTune constructor\n   *\n   * @param {API} api - Editor's API\n   */\n  constructor({ api }) {\n    this.api = api;\n  }\n\n  /**\n   * Tune's appearance in block settings menu\n   */\n  public render(): MenuConfig {\n    return {\n      icon: IconCross,\n      title: this.api.i18n.t('Delete'),\n      name: 'delete',\n      confirmation: {\n        title: this.api.i18n.t('Click to delete'),\n        onActivate: (): void => this.handleClick(),\n      },\n    };\n  }\n\n  /**\n   * Delete block conditions passed\n   */\n  public handleClick(): void {\n    this.api.blocks.delete();\n  }\n}\n"
  },
  {
    "path": "src/components/block-tunes/block-tune-move-down.ts",
    "content": "/**\n * @class MoveDownTune\n * @classdesc Editor's default tune - Moves down highlighted block\n * @copyright <CodeX Team> 2018\n */\n\nimport type { API, BlockTune } from '../../../types';\nimport { IconChevronDown } from '@codexteam/icons';\nimport type { TunesMenuConfig } from '../../../types/tools';\n\n\n/**\n *\n */\nexport default class MoveDownTune implements BlockTune {\n  /**\n   * Set Tool is Tune\n   */\n  public static readonly isTune = true;\n\n  /**\n   * Property that contains Editor.js API methods\n   *\n   * @see {@link docs/api.md}\n   */\n  private readonly api: API;\n\n  /**\n   * Styles\n   */\n  private CSS = {\n    animation: 'wobble',\n  };\n\n  /**\n   * MoveDownTune constructor\n   *\n   * @param {API} api — Editor's API\n   */\n  constructor({ api }) {\n    this.api = api;\n  }\n\n  /**\n   * Tune's appearance in block settings menu\n   */\n  public render(): TunesMenuConfig {\n    return {\n      icon: IconChevronDown,\n      title: this.api.i18n.t('Move down'),\n      onActivate: (): void => this.handleClick(),\n      name: 'move-down',\n    };\n  }\n\n  /**\n   * Handle clicks on 'move down' button\n   */\n  public handleClick(): void {\n    const currentBlockIndex = this.api.blocks.getCurrentBlockIndex();\n    const nextBlock = this.api.blocks.getBlockByIndex(currentBlockIndex + 1);\n\n    // If Block is last do nothing\n    if (!nextBlock) {\n      throw new Error('Unable to move Block down since it is already the last');\n    }\n\n    const nextBlockElement = nextBlock.holder;\n    const nextBlockCoords = nextBlockElement.getBoundingClientRect();\n\n    let scrollOffset = Math.abs(window.innerHeight - nextBlockElement.offsetHeight);\n\n    /**\n     * Next block ends on screen.\n     * Increment scroll by next block's height to save element onscreen-position\n     */\n    if (nextBlockCoords.top < window.innerHeight) {\n      scrollOffset = window.scrollY + nextBlockElement.offsetHeight;\n    }\n\n    window.scrollTo(0, scrollOffset);\n\n    /** Change blocks positions */\n    this.api.blocks.move(currentBlockIndex + 1);\n\n    this.api.toolbar.toggleBlockSettings(true);\n  }\n}\n"
  },
  {
    "path": "src/components/block-tunes/block-tune-move-up.ts",
    "content": "/**\n * @class MoveUpTune\n * @classdesc Editor's default tune that moves up selected block\n * @copyright <CodeX Team> 2018\n */\nimport type { API, BlockTune } from '../../../types';\nimport { IconChevronUp } from '@codexteam/icons';\nimport type { TunesMenuConfig } from '../../../types/tools';\n\n/**\n *\n */\nexport default class MoveUpTune implements BlockTune {\n  /**\n   * Set Tool is Tune\n   */\n  public static readonly isTune = true;\n\n  /**\n   * Property that contains Editor.js API methods\n   *\n   * @see {@link docs/api.md}\n   */\n  private readonly api: API;\n\n  /**\n   * Styles\n   */\n  private CSS = {\n    animation: 'wobble',\n  };\n\n  /**\n   * MoveUpTune constructor\n   *\n   * @param {API} api - Editor's API\n   */\n  constructor({ api }) {\n    this.api = api;\n  }\n\n  /**\n   * Tune's appearance in block settings menu\n   */\n  public render(): TunesMenuConfig {\n    return {\n      icon: IconChevronUp,\n      title: this.api.i18n.t('Move up'),\n      onActivate: (): void => this.handleClick(),\n      name: 'move-up',\n    };\n  }\n\n  /**\n   * Move current block up\n   */\n  public handleClick(): void {\n    const currentBlockIndex = this.api.blocks.getCurrentBlockIndex();\n    const currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex);\n    const previousBlock = this.api.blocks.getBlockByIndex(currentBlockIndex - 1);\n\n    if (currentBlockIndex === 0 || !currentBlock || !previousBlock) {\n      throw new Error('Unable to move Block up since it is already the first');\n    }\n\n    const currentBlockElement = currentBlock.holder;\n    const previousBlockElement = previousBlock.holder;\n\n    /**\n     * Here is two cases:\n     *  - when previous block has negative offset and part of it is visible on window, then we scroll\n     *  by window's height and add offset which is mathematically difference between two blocks\n     *\n     *  - when previous block is visible and has offset from the window,\n     *      than we scroll window to the difference between this offsets.\n     */\n    const currentBlockCoords = currentBlockElement.getBoundingClientRect(),\n        previousBlockCoords = previousBlockElement.getBoundingClientRect();\n\n    let scrollUpOffset;\n\n    if (previousBlockCoords.top > 0) {\n      scrollUpOffset = Math.abs(currentBlockCoords.top) - Math.abs(previousBlockCoords.top);\n    } else {\n      scrollUpOffset = Math.abs(currentBlockCoords.top) + previousBlockCoords.height;\n    }\n\n    window.scrollBy(0, -1 * scrollUpOffset);\n\n    /** Change blocks positions */\n    this.api.blocks.move(currentBlockIndex - 1);\n\n    this.api.toolbar.toggleBlockSettings(true);\n  }\n}\n"
  },
  {
    "path": "src/components/blocks.ts",
    "content": "import * as _ from './utils';\nimport $ from './dom';\nimport type Block from './block';\nimport { BlockToolAPI } from './block';\nimport type { MoveEvent } from '../../types/tools';\n\n/**\n * @class Blocks\n * @classdesc Class to work with Block instances array\n * @private\n * @property {HTMLElement} workingArea — editor`s working node\n */\nexport default class Blocks {\n  /**\n   * Array of Block instances in order of addition\n   */\n  public blocks: Block[];\n\n  /**\n   * Editor`s area where to add Block`s HTML\n   */\n  public workingArea: HTMLElement;\n\n  /**\n   * @class\n   * @param {HTMLElement} workingArea — editor`s working node\n   */\n  constructor(workingArea: HTMLElement) {\n    this.blocks = [];\n    this.workingArea = workingArea;\n  }\n\n  /**\n   * Get length of Block instances array\n   *\n   * @returns {number}\n   */\n  public get length(): number {\n    return this.blocks.length;\n  }\n\n  /**\n   * Get Block instances array\n   *\n   * @returns {Block[]}\n   */\n  public get array(): Block[] {\n    return this.blocks;\n  }\n\n  /**\n   * Get blocks html elements array\n   *\n   * @returns {HTMLElement[]}\n   */\n  public get nodes(): HTMLElement[] {\n    return _.array(this.workingArea.children);\n  }\n\n  /**\n   * Proxy trap to implement array-like setter\n   *\n   * @example\n   * blocks[0] = new Block(...)\n   * @param {Blocks} instance — Blocks instance\n   * @param {PropertyKey} property — block index or any Blocks class property key to set\n   * @param {Block} value — value to set\n   * @returns {boolean}\n   */\n  public static set(instance: Blocks, property: PropertyKey, value: Block | unknown): boolean {\n    /**\n     * If property name is not a number (method or other property, access it via reflect\n     */\n    if (isNaN(Number(property))) {\n      Reflect.set(instance, property, value);\n\n      return true;\n    }\n\n    /**\n     * If property is number, call insert method to emulate array behaviour\n     *\n     * @example\n     * blocks[0] = new Block();\n     */\n    instance.insert(+(property as number), value as Block);\n\n    return true;\n  }\n\n  /**\n   * Proxy trap to implement array-like getter\n   *\n   * @param {Blocks} instance — Blocks instance\n   * @param {PropertyKey} property — Blocks class property key\n   * @returns {Block|*}\n   */\n  public static get(instance: Blocks, property: PropertyKey): Block | unknown {\n    /**\n     * If property is not a number, get it via Reflect object\n     */\n    if (isNaN(Number(property))) {\n      return Reflect.get(instance, property);\n    }\n\n    /**\n     * If property is a number (Block index) return Block by passed index\n     */\n    return instance.get(+(property as number));\n  }\n\n  /**\n   * Push new Block to the blocks array and append it to working area\n   *\n   * @param {Block} block - Block to add\n   */\n  public push(block: Block): void {\n    this.blocks.push(block);\n    this.insertToDOM(block);\n  }\n\n  /**\n   * Swaps blocks with indexes first and second\n   *\n   * @param {number} first - first block index\n   * @param {number} second - second block index\n   * @deprecated — use 'move' instead\n   */\n  public swap(first: number, second: number): void {\n    const secondBlock = this.blocks[second];\n\n    /**\n     * Change in DOM\n     */\n    $.swap(this.blocks[first].holder, secondBlock.holder);\n\n    /**\n     * Change in array\n     */\n    this.blocks[second] = this.blocks[first];\n    this.blocks[first] = secondBlock;\n  }\n\n  /**\n   * Move a block from one to another index\n   *\n   * @param {number} toIndex - new index of the block\n   * @param {number} fromIndex - block to move\n   */\n  public move(toIndex: number, fromIndex: number): void {\n    /**\n     * cut out the block, move the DOM element and insert at the desired index\n     * again (the shifting within the blocks array will happen automatically).\n     *\n     * @see https://stackoverflow.com/a/44932690/1238150\n     */\n    const block = this.blocks.splice(fromIndex, 1)[0];\n\n    // manipulate DOM\n    const prevIndex = toIndex - 1;\n    const previousBlockIndex = Math.max(0, prevIndex);\n    const previousBlock = this.blocks[previousBlockIndex];\n\n    if (toIndex > 0) {\n      this.insertToDOM(block, 'afterend', previousBlock);\n    } else {\n      this.insertToDOM(block, 'beforebegin', previousBlock);\n    }\n\n    // move in array\n    this.blocks.splice(toIndex, 0, block);\n\n    // invoke hook\n    const event: MoveEvent = this.composeBlockEvent('move', {\n      fromIndex,\n      toIndex,\n    });\n\n    block.call(BlockToolAPI.MOVED, event);\n  }\n\n  /**\n   * Insert new Block at passed index\n   *\n   * @param {number} index — index to insert Block\n   * @param {Block} block — Block to insert\n   * @param {boolean} replace — it true, replace block on given index\n   */\n  public insert(index: number, block: Block, replace = false): void {\n    if (!this.length) {\n      this.push(block);\n\n      return;\n    }\n\n    if (index > this.length) {\n      index = this.length;\n    }\n\n    if (replace) {\n      this.blocks[index].holder.remove();\n      this.blocks[index].call(BlockToolAPI.REMOVED);\n    }\n\n    const deleteCount = replace ? 1 : 0;\n\n    this.blocks.splice(index, deleteCount, block);\n\n    if (index > 0) {\n      const previousBlock = this.blocks[index - 1];\n\n      this.insertToDOM(block, 'afterend', previousBlock);\n    } else {\n      const nextBlock = this.blocks[index + 1];\n\n      if (nextBlock) {\n        this.insertToDOM(block, 'beforebegin', nextBlock);\n      } else {\n        this.insertToDOM(block);\n      }\n    }\n  }\n\n  /**\n   * Replaces block under passed index with passed block\n   *\n   * @param index - index of existed block\n   * @param block - new block\n   */\n  public replace(index: number, block: Block): void {\n    if (this.blocks[index] === undefined) {\n      throw Error('Incorrect index');\n    }\n\n    const prevBlock = this.blocks[index];\n\n    prevBlock.holder.replaceWith(block.holder);\n\n    this.blocks[index] = block;\n  }\n\n  /**\n   * Inserts several blocks at once\n   *\n   * @param blocks - blocks to insert\n   * @param index - index to insert blocks at\n   */\n  public insertMany(blocks: Block[], index: number ): void {\n    const fragment = new DocumentFragment();\n\n    for (const block of blocks) {\n      fragment.appendChild(block.holder);\n    }\n\n    if (this.length > 0) {\n      if (index > 0) {\n        const previousBlockIndex = Math.min(index - 1, this.length - 1);\n        const previousBlock = this.blocks[previousBlockIndex];\n\n        previousBlock.holder.after(fragment);\n      } else if (index === 0) {\n        this.workingArea.prepend(fragment);\n      }\n\n      /**\n       * Insert blocks to the array at the specified index\n       */\n      this.blocks.splice(index, 0, ...blocks);\n    } else {\n      this.blocks.push(...blocks);\n      this.workingArea.appendChild(fragment);\n    }\n\n    /**\n     * Call Rendered event for each block\n     */\n    blocks.forEach((block) => block.call(BlockToolAPI.RENDERED));\n  }\n\n  /**\n   * Remove block\n   *\n   * @param {number} index - index of Block to remove\n   */\n  public remove(index: number): void {\n    if (isNaN(index)) {\n      index = this.length - 1;\n    }\n\n    this.blocks[index].holder.remove();\n\n    this.blocks[index].call(BlockToolAPI.REMOVED);\n\n    this.blocks.splice(index, 1);\n  }\n\n  /**\n   * Remove all blocks\n   */\n  public removeAll(): void {\n    this.workingArea.innerHTML = '';\n\n    this.blocks.forEach((block) => block.call(BlockToolAPI.REMOVED));\n\n    this.blocks.length = 0;\n  }\n\n  /**\n   * Insert Block after passed target\n   *\n   * @todo decide if this method is necessary\n   * @param {Block} targetBlock — target after which Block should be inserted\n   * @param {Block} newBlock — Block to insert\n   */\n  public insertAfter(targetBlock: Block, newBlock: Block): void {\n    const index = this.blocks.indexOf(targetBlock);\n\n    this.insert(index + 1, newBlock);\n  }\n\n  /**\n   * Get Block by index\n   *\n   * @param {number} index — Block index\n   * @returns {Block}\n   */\n  public get(index: number): Block | undefined {\n    return this.blocks[index];\n  }\n\n  /**\n   * Return index of passed Block\n   *\n   * @param {Block} block - Block to find\n   * @returns {number}\n   */\n  public indexOf(block: Block): number {\n    return this.blocks.indexOf(block);\n  }\n\n  /**\n   * Insert new Block into DOM\n   *\n   * @param {Block} block - Block to insert\n   * @param {InsertPosition} position — insert position (if set, will use insertAdjacentElement)\n   * @param {Block} target — Block related to position\n   */\n  private insertToDOM(block: Block, position?: InsertPosition, target?: Block): void {\n    if (position) {\n      target.holder.insertAdjacentElement(position, block.holder);\n    } else {\n      this.workingArea.appendChild(block.holder);\n    }\n\n    block.call(BlockToolAPI.RENDERED);\n  }\n\n  /**\n   * Composes Block event with passed type and details\n   *\n   * @param {string} type - event type\n   * @param {object} detail - event detail\n   */\n  private composeBlockEvent(type: string, detail: object): MoveEvent {\n    return new CustomEvent(type, {\n      detail,\n    }) as MoveEvent;\n  }\n}\n"
  },
  {
    "path": "src/components/constants.ts",
    "content": "/**\n * Debounce timeout for selection change event\n * {@link modules/ui.ts}\n */\nexport const selectionChangeDebounceTimeout = 180;\n\n/**\n * Timeout for batching of DOM changes used by the ModificationObserver\n * {@link modules/modificationsObserver.ts}\n */\nexport const modificationsObserverBatchTimeout = 400;\n"
  },
  {
    "path": "src/components/core.ts",
    "content": "import $ from './dom';\nimport * as _ from './utils';\nimport type { EditorConfig, SanitizerConfig } from '../../types';\nimport type { EditorModules } from '../types-internal/editor-modules';\nimport I18n from './i18n';\nimport { CriticalError } from './errors/critical';\nimport EventsDispatcher from './utils/events';\nimport Modules from './modules';\nimport type { EditorEventMap } from './events';\n\n/**\n * Editor.js core class. Bootstraps modules.\n */\nexport default class Core {\n  /**\n   * Editor configuration passed by user to the constructor\n   */\n  public config: EditorConfig;\n\n  /**\n   * Object with core modules instances\n   */\n  public moduleInstances: EditorModules = {} as EditorModules;\n\n  /**\n   * Promise that resolves when all core modules are prepared and UI is rendered on the page\n   */\n  public isReady: Promise<void>;\n\n  /**\n   * Common Editor Event Bus\n   */\n  private eventsDispatcher: EventsDispatcher<EditorEventMap> = new EventsDispatcher();\n\n  /**\n   * @param {EditorConfig} config - user configuration\n   */\n  constructor(config?: EditorConfig|string) {\n    /**\n     * Ready promise. Resolved if Editor.js is ready to work, rejected otherwise\n     */\n    let onReady: (value?: void | PromiseLike<void>) => void;\n    let onFail: (reason?: unknown) => void;\n\n    this.isReady = new Promise((resolve, reject) => {\n      onReady = resolve;\n      onFail = reject;\n    });\n\n    Promise.resolve()\n      .then(async () => {\n        this.configuration = config;\n\n        this.validate();\n        this.init();\n        await this.start();\n        await this.render();\n\n        const { BlockManager, Caret, UI, ModificationsObserver } = this.moduleInstances;\n\n        UI.checkEmptiness();\n        ModificationsObserver.enable();\n\n        if ((this.configuration as EditorConfig).autofocus === true && this.configuration.readOnly !== true) {\n          Caret.setToBlock(BlockManager.blocks[0], Caret.positions.START);\n        }\n\n        onReady();\n      })\n      .catch((error) => {\n        _.log(`Editor.js is not ready because of ${error}`, 'error');\n\n        /**\n         * Reject this.isReady promise\n         */\n        onFail(error);\n      });\n  }\n\n  /**\n   * Setting for configuration\n   *\n   * @param {EditorConfig|string} config - Editor's config to set\n   */\n  public set configuration(config: EditorConfig|string) {\n    /**\n     * Place config into the class property\n     *\n     * @type {EditorConfig}\n     */\n    if (_.isObject(config)) {\n      this.config = {\n        ...config,\n      };\n    } else {\n      /**\n       * Process zero-configuration or with only holderId\n       * Make config object\n       */\n      this.config = {\n        holder: config,\n      };\n    }\n\n    /**\n     * If holderId is preset, assign him to holder property and work next only with holder\n     */\n    _.deprecationAssert(!!this.config.holderId, 'config.holderId', 'config.holder');\n    if (this.config.holderId && !this.config.holder) {\n      this.config.holder = this.config.holderId;\n      this.config.holderId = null;\n    }\n\n    /**\n     * If holder is empty then set a default value\n     */\n    if (this.config.holder == null) {\n      this.config.holder = 'editorjs';\n    }\n\n    if (!this.config.logLevel) {\n      this.config.logLevel = _.LogLevels.VERBOSE;\n    }\n\n    _.setLogLevel(this.config.logLevel);\n\n    /**\n     * If default Block's Tool was not passed, use the Paragraph Tool\n     */\n    _.deprecationAssert(Boolean(this.config.initialBlock), 'config.initialBlock', 'config.defaultBlock');\n    this.config.defaultBlock = this.config.defaultBlock || this.config.initialBlock || 'paragraph';\n\n    /**\n     * Height of Editor's bottom area that allows to set focus on the last Block\n     *\n     * @type {number}\n     */\n    // eslint-disable-next-line @typescript-eslint/no-magic-numbers\n    this.config.minHeight = this.config.minHeight !== undefined ? this.config.minHeight : 300;\n\n    /**\n     * Default block type\n     * Uses in case when there is no blocks passed\n     *\n     * @type {{type: (*), data: {text: null}}}\n     */\n    const defaultBlockData = {\n      type: this.config.defaultBlock,\n      data: {},\n    };\n\n    this.config.placeholder = this.config.placeholder || false;\n    this.config.sanitizer = this.config.sanitizer || {\n      p: true,\n      b: true,\n      a: true,\n    } as SanitizerConfig;\n\n    this.config.hideToolbar = this.config.hideToolbar ? this.config.hideToolbar : false;\n    this.config.tools = this.config.tools || {};\n    this.config.i18n = this.config.i18n || {};\n    this.config.data = this.config.data || { blocks: [] };\n    // eslint-disable-next-line @typescript-eslint/no-empty-function\n    this.config.onReady = this.config.onReady || ((): void => {});\n    // eslint-disable-next-line @typescript-eslint/no-empty-function\n    this.config.onChange = this.config.onChange || ((): void => {});\n    this.config.inlineToolbar = this.config.inlineToolbar !== undefined ? this.config.inlineToolbar : true;\n\n    /**\n     * Initialize default Block to pass data to the Renderer\n     */\n    if (_.isEmpty(this.config.data) || !this.config.data.blocks || this.config.data.blocks.length === 0) {\n      this.config.data = { blocks: [ defaultBlockData ] };\n    }\n\n    this.config.readOnly = this.config.readOnly as boolean || false;\n\n    /**\n     * Adjust i18n\n     */\n    if (this.config.i18n?.messages) {\n      I18n.setDictionary(this.config.i18n.messages);\n    }\n\n    /**\n     * Text direction. If not set, uses ltr\n     */\n    this.config.i18n.direction = this.config.i18n?.direction || 'ltr';\n  }\n\n  /**\n   * Returns private property\n   *\n   * @returns {EditorConfig}\n   */\n  public get configuration(): EditorConfig {\n    return this.config;\n  }\n\n  /**\n   * Checks for required fields in Editor's config\n   */\n  public validate(): void {\n    const { holderId, holder } = this.config;\n\n    if (holderId && holder) {\n      throw Error('«holderId» and «holder» param can\\'t assign at the same time.');\n    }\n\n    /**\n     * Check for a holder element's existence\n     */\n    if (_.isString(holder) && !$.get(holder)) {\n      throw Error(`element with ID «${holder}» is missing. Pass correct holder's ID.`);\n    }\n\n    if (holder && _.isObject(holder) && !$.isElement(holder)) {\n      throw Error('«holder» value must be an Element node');\n    }\n  }\n\n  /**\n   * Initializes modules:\n   *  - make and save instances\n   *  - configure\n   */\n  public init(): void {\n    /**\n     * Make modules instances and save it to the @property this.moduleInstances\n     */\n    this.constructModules();\n\n    /**\n     * Modules configuration\n     */\n    this.configureModules();\n  }\n\n  /**\n   * Start Editor!\n   *\n   * Get list of modules that needs to be prepared and return a sequence (Promise)\n   *\n   * @returns {Promise<void>}\n   */\n  public async start(): Promise<void> {\n    const modulesToPrepare = [\n      'Tools',\n      'UI',\n      'BlockManager',\n      'Paste',\n      'BlockSelection',\n      'RectangleSelection',\n      'CrossBlockSelection',\n      'ReadOnly',\n    ];\n\n    await modulesToPrepare.reduce(\n      (promise, module) => promise.then(async () => {\n        // _.log(`Preparing ${module} module`, 'time');\n\n        try {\n          await this.moduleInstances[module].prepare();\n        } catch (e) {\n          /**\n           * CriticalError's will not be caught\n           * It is used when Editor is rendering in read-only mode with unsupported plugin\n           */\n          if (e instanceof CriticalError) {\n            throw new Error(e.message);\n          }\n          _.log(`Module ${module} was skipped because of %o`, 'warn', e);\n        }\n        // _.log(`Preparing ${module} module`, 'timeEnd');\n      }),\n      Promise.resolve()\n    );\n  }\n\n  /**\n   * Render initial data\n   */\n  private render(): Promise<void> {\n    return this.moduleInstances.Renderer.render(this.config.data.blocks);\n  }\n\n  /**\n   * Make modules instances and save it to the @property this.moduleInstances\n   */\n  private constructModules(): void {\n    Object.entries(Modules).forEach(([key, module]) => {\n      try {\n        this.moduleInstances[key] = new module({\n          config: this.configuration,\n          eventsDispatcher: this.eventsDispatcher,\n        });\n      } catch (e) {\n        _.log('[constructModules]', `Module ${key} skipped because`, 'error', e);\n      }\n    });\n  }\n\n  /**\n   * Modules instances configuration:\n   *  - pass other modules to the 'state' property\n   *  - ...\n   */\n  private configureModules(): void {\n    for (const name in this.moduleInstances) {\n      if (Object.prototype.hasOwnProperty.call(this.moduleInstances, name)) {\n        /**\n         * Module does not need self-instance\n         */\n        this.moduleInstances[name].state = this.getModulesDiff(name);\n      }\n    }\n  }\n\n  /**\n   * Return modules without passed name\n   *\n   * @param {string} name - module for witch modules difference should be calculated\n   */\n  private getModulesDiff(name: string): EditorModules {\n    const diff = {} as EditorModules;\n\n    for (const moduleName in this.moduleInstances) {\n      /**\n       * Skip module with passed name\n       */\n      if (moduleName === name) {\n        continue;\n      }\n      diff[moduleName] = this.moduleInstances[moduleName];\n    }\n\n    return diff;\n  }\n}\n"
  },
  {
    "path": "src/components/dom.ts",
    "content": "import * as _ from './utils';\n\n/**\n * DOM manipulations helper\n *\n * @todo get rid of class and make separate utility functions\n */\nexport default class Dom {\n  /**\n   * Check if passed tag has no closed tag\n   *\n   * @param {HTMLElement} tag - element to check\n   * @returns {boolean}\n   */\n  public static isSingleTag(tag: HTMLElement): boolean {\n    return tag.tagName && [\n      'AREA',\n      'BASE',\n      'BR',\n      'COL',\n      'COMMAND',\n      'EMBED',\n      'HR',\n      'IMG',\n      'INPUT',\n      'KEYGEN',\n      'LINK',\n      'META',\n      'PARAM',\n      'SOURCE',\n      'TRACK',\n      'WBR',\n    ].includes(tag.tagName);\n  }\n\n  /**\n   * Check if element is BR or WBR\n   *\n   * @param {HTMLElement} element - element to check\n   * @returns {boolean}\n   */\n  public static isLineBreakTag(element: HTMLElement): element is HTMLBRElement {\n    return element && element.tagName && [\n      'BR',\n      'WBR',\n    ].includes(element.tagName);\n  }\n\n  /**\n   * Helper for making Elements with class name and attributes\n   *\n   * @param  {string} tagName - new Element tag name\n   * @param  {string[]|string} [classNames] - list or name of CSS class name(s)\n   * @param  {object} [attributes] - any attributes\n   * @returns {HTMLElement}\n   */\n  public static make(tagName: string, classNames: string | (string | undefined)[] | null = null, attributes: object = {}): HTMLElement {\n    const el = document.createElement(tagName);\n\n    if (Array.isArray(classNames)) {\n      const validClassnames = classNames.filter(className => className !== undefined) as string[];\n\n      el.classList.add(...validClassnames);\n    } else if (classNames) {\n      el.classList.add(classNames);\n    }\n\n    for (const attrName in attributes) {\n      if (Object.prototype.hasOwnProperty.call(attributes, attrName)) {\n        el[attrName] = attributes[attrName];\n      }\n    }\n\n    return el;\n  }\n\n  /**\n   * Creates Text Node with the passed content\n   *\n   * @param {string} content - text content\n   * @returns {Text}\n   */\n  public static text(content: string): Text {\n    return document.createTextNode(content);\n  }\n\n  /**\n   * Append one or several elements to the parent\n   *\n   * @param  {Element|DocumentFragment} parent - where to append\n   * @param  {Element|Element[]|DocumentFragment|Text|Text[]} elements - element or elements list\n   */\n  public static append(\n    parent: Element | DocumentFragment,\n    elements: Element | Element[] | DocumentFragment | Text | Text[]\n  ): void {\n    if (Array.isArray(elements)) {\n      elements.forEach((el) => parent.appendChild(el));\n    } else {\n      parent.appendChild(elements);\n    }\n  }\n\n  /**\n   * Append element or a couple to the beginning of the parent elements\n   *\n   * @param {Element} parent - where to append\n   * @param {Element|Element[]} elements - element or elements list\n   */\n  public static prepend(parent: Element, elements: Element | Element[]): void {\n    if (Array.isArray(elements)) {\n      elements = elements.reverse();\n      elements.forEach((el) => parent.prepend(el));\n    } else {\n      parent.prepend(elements);\n    }\n  }\n\n  /**\n   * Swap two elements in parent\n   *\n   * @param {HTMLElement} el1 - from\n   * @param {HTMLElement} el2 - to\n   * @deprecated\n   */\n  public static swap(el1: HTMLElement, el2: HTMLElement): void {\n    // create marker element and insert it where el1 is\n    const temp = document.createElement('div'),\n        parent = el1.parentNode;\n\n    parent.insertBefore(temp, el1);\n\n    // move el1 to right before el2\n    parent.insertBefore(el1, el2);\n\n    // move el2 to right before where el1 used to be\n    parent.insertBefore(el2, temp);\n\n    // remove temporary marker node\n    parent.removeChild(temp);\n  }\n\n  /**\n   * Selector Decorator\n   *\n   * Returns first match\n   *\n   * @param {Element} el - element we searching inside. Default - DOM Document\n   * @param {string} selector - searching string\n   * @returns {Element}\n   */\n  public static find(el: Element | Document = document, selector: string): Element | null {\n    return el.querySelector(selector);\n  }\n\n  /**\n   * Get Element by Id\n   *\n   * @param {string} id - id to find\n   * @returns {HTMLElement | null}\n   */\n  public static get(id: string): HTMLElement | null {\n    return document.getElementById(id);\n  }\n\n  /**\n   * Selector Decorator.\n   *\n   * Returns all matches\n   *\n   * @param {Element|Document} el - element we searching inside. Default - DOM Document\n   * @param {string} selector - searching string\n   * @returns {NodeList}\n   */\n  public static findAll(el: Element | Document = document, selector: string): NodeList {\n    return el.querySelectorAll(selector);\n  }\n\n  /**\n   * Returns CSS selector for all text inputs\n   */\n  public static get allInputsSelector(): string {\n    const allowedInputTypes = ['text', 'password', 'email', 'number', 'search', 'tel', 'url'];\n\n    return '[contenteditable=true], textarea, input:not([type]), ' +\n      allowedInputTypes.map((type) => `input[type=\"${type}\"]`).join(', ');\n  }\n\n  /**\n   * Find all contenteditable, textarea and editable input elements passed holder contains\n   *\n   * @param holder - element where to find inputs\n   */\n  public static findAllInputs(holder: Element): HTMLElement[] {\n    return _.array(holder.querySelectorAll(Dom.allInputsSelector))\n      /**\n       * If contenteditable element contains block elements, treat them as inputs.\n       */\n      .reduce((result, input) => {\n        if (Dom.isNativeInput(input) || Dom.containsOnlyInlineElements(input)) {\n          return [...result, input];\n        }\n\n        return [...result, ...Dom.getDeepestBlockElements(input)];\n      }, []);\n  }\n\n  /**\n   * Search for deepest node which is Leaf.\n   * Leaf is the vertex that doesn't have any child nodes\n   *\n   * @description Method recursively goes throw the all Node until it finds the Leaf\n   * @param {Node} node - root Node. From this vertex we start Deep-first search\n   *                      {@link https://en.wikipedia.org/wiki/Depth-first_search}\n   * @param {boolean} [atLast] - find last text node\n   * @returns - it can be text Node or Element Node, so that caret will able to work with it\n   *            Can return null if node is Document or DocumentFragment, or node is not attached to the DOM\n   */\n  public static getDeepestNode(node: Node, atLast = false): Node | null {\n    /**\n     * Current function have two directions:\n     *  - starts from first child and every time gets first or nextSibling in special cases\n     *  - starts from last child and gets last or previousSibling\n     *\n     * @type {string}\n     */\n    const child = atLast ? 'lastChild' : 'firstChild',\n        sibling = atLast ? 'previousSibling' : 'nextSibling';\n\n    if (node && node.nodeType === Node.ELEMENT_NODE && node[child]) {\n      let nodeChild = node[child] as Node;\n\n      /**\n       * special case when child is single tag that can't contain any content\n       */\n      if (\n        Dom.isSingleTag(nodeChild as HTMLElement) &&\n        !Dom.isNativeInput(nodeChild) &&\n        !Dom.isLineBreakTag(nodeChild as HTMLElement)\n      ) {\n        /**\n         * 1) We need to check the next sibling. If it is Node Element then continue searching for deepest\n         * from sibling\n         *\n         * 2) If single tag's next sibling is null, then go back to parent and check his sibling\n         * In case of Node Element continue searching\n         *\n         * 3) If none of conditions above happened return parent Node Element\n         */\n        if (nodeChild[sibling]) {\n          nodeChild = nodeChild[sibling];\n        } else if (nodeChild.parentNode[sibling]) {\n          nodeChild = nodeChild.parentNode[sibling];\n        } else {\n          return nodeChild.parentNode;\n        }\n      }\n\n      return this.getDeepestNode(nodeChild, atLast);\n    }\n\n    return node;\n  }\n\n  /**\n   * Check if object is DOM node\n   *\n   * @param {*} node - object to check\n   * @returns {boolean}\n   */\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  public static isElement(node: any): node is Element {\n    if (_.isNumber(node)) {\n      return false;\n    }\n\n    return node && node.nodeType && node.nodeType === Node.ELEMENT_NODE;\n  }\n\n  /**\n   * Check if object is DocumentFragment node\n   *\n   * @param {object} node - object to check\n   * @returns {boolean}\n   */\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  public static isFragment(node: any): node is DocumentFragment {\n    if (_.isNumber(node)) {\n      return false;\n    }\n\n    return node && node.nodeType && node.nodeType === Node.DOCUMENT_FRAGMENT_NODE;\n  }\n\n  /**\n   * Check if passed element is contenteditable\n   *\n   * @param {HTMLElement} element - html element to check\n   * @returns {boolean}\n   */\n  public static isContentEditable(element: HTMLElement): boolean {\n    return element.contentEditable === 'true';\n  }\n\n  /**\n   * Checks target if it is native input\n   *\n   * @param {*} target - HTML element or string\n   * @returns {boolean}\n   */\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  public static isNativeInput(target: any): target is HTMLInputElement | HTMLTextAreaElement {\n    const nativeInputs = [\n      'INPUT',\n      'TEXTAREA',\n    ];\n\n    return target && target.tagName ? nativeInputs.includes(target.tagName) : false;\n  }\n\n  /**\n   * Checks if we can set caret\n   *\n   * @param {HTMLElement} target - target to check\n   * @returns {boolean}\n   */\n  public static canSetCaret(target: HTMLElement): boolean {\n    let result = true;\n\n    if (Dom.isNativeInput(target)) {\n      switch (target.type) {\n        case 'file':\n        case 'checkbox':\n        case 'radio':\n        case 'hidden':\n        case 'submit':\n        case 'button':\n        case 'image':\n        case 'reset':\n          result = false;\n          break;\n      }\n    } else {\n      result = Dom.isContentEditable(target);\n    }\n\n    return result;\n  }\n\n  /**\n   * Checks node if it is empty\n   *\n   * @description Method checks simple Node without any childs for emptiness\n   * If you have Node with 2 or more children id depth, you better use {@link Dom#isEmpty} method\n   * @param {Node} node - node to check\n   * @param {string} [ignoreChars] - char or substring to treat as empty\n   * @returns {boolean} true if it is empty\n   */\n  public static isNodeEmpty(node: Node, ignoreChars?: string): boolean {\n    let nodeText;\n\n    if (this.isSingleTag(node as HTMLElement) && !this.isLineBreakTag(node as HTMLElement)) {\n      return false;\n    }\n\n    if (this.isElement(node) && this.isNativeInput(node)) {\n      nodeText = (node as HTMLInputElement).value;\n    } else {\n      nodeText = node.textContent.replace('\\u200B', '');\n    }\n\n    if (ignoreChars) {\n      nodeText = nodeText.replace(new RegExp(ignoreChars, 'g'), '');\n    }\n\n    return nodeText.length === 0;\n  }\n\n  /**\n   * checks node if it is doesn't have any child nodes\n   *\n   * @param {Node} node - node to check\n   * @returns {boolean}\n   */\n  public static isLeaf(node: Node): boolean {\n    if (!node) {\n      return false;\n    }\n\n    return node.childNodes.length === 0;\n  }\n\n  /**\n   * breadth-first search (BFS)\n   * {@link https://en.wikipedia.org/wiki/Breadth-first_search}\n   *\n   * @description Pushes to stack all DOM leafs and checks for emptiness\n   * @param {Node} node - node to check\n   * @param {string} [ignoreChars] - char or substring to treat as empty\n   * @returns {boolean}\n   */\n  public static isEmpty(node: Node, ignoreChars?: string): boolean {\n    const treeWalker = [ node ];\n\n    while (treeWalker.length > 0) {\n      node = treeWalker.shift();\n\n      if (!node) {\n        continue;\n      }\n\n      if (this.isLeaf(node) && !this.isNodeEmpty(node, ignoreChars)) {\n        return false;\n      }\n\n      if (node.childNodes) {\n        treeWalker.push(...Array.from(node.childNodes));\n      }\n    }\n\n    return true;\n  }\n\n  /**\n   * Check if string contains html elements\n   *\n   * @param {string} str - string to check\n   * @returns {boolean}\n   */\n  public static isHTMLString(str: string): boolean {\n    const wrapper = Dom.make('div');\n\n    wrapper.innerHTML = str;\n\n    return wrapper.childElementCount > 0;\n  }\n\n  /**\n   * Return length of node`s text content\n   *\n   * @param {Node} node - node with content\n   * @returns {number}\n   */\n  public static getContentLength(node: Node): number {\n    if (Dom.isNativeInput(node)) {\n      return (node as HTMLInputElement).value.length;\n    }\n\n    if (node.nodeType === Node.TEXT_NODE) {\n      return (node as Text).length;\n    }\n\n    return node.textContent.length;\n  }\n\n  /**\n   * Return array of names of block html elements\n   *\n   * @returns {string[]}\n   */\n  public static get blockElements(): string[] {\n    return [\n      'address',\n      'article',\n      'aside',\n      'blockquote',\n      'canvas',\n      'div',\n      'dl',\n      'dt',\n      'fieldset',\n      'figcaption',\n      'figure',\n      'footer',\n      'form',\n      'h1',\n      'h2',\n      'h3',\n      'h4',\n      'h5',\n      'h6',\n      'header',\n      'hgroup',\n      'hr',\n      'li',\n      'main',\n      'nav',\n      'noscript',\n      'ol',\n      'output',\n      'p',\n      'pre',\n      'ruby',\n      'section',\n      'table',\n      'tbody',\n      'thead',\n      'tr',\n      'tfoot',\n      'ul',\n      'video',\n    ];\n  }\n\n  /**\n   * Check if passed content includes only inline elements\n   *\n   * @param {string|HTMLElement} data - element or html string\n   * @returns {boolean}\n   */\n  public static containsOnlyInlineElements(data: string | HTMLElement): boolean {\n    let wrapper: HTMLElement;\n\n    if (_.isString(data)) {\n      wrapper = document.createElement('div');\n      wrapper.innerHTML = data;\n    } else {\n      wrapper = data;\n    }\n\n    const check = (element: HTMLElement): boolean => {\n      return !Dom.blockElements.includes(element.tagName.toLowerCase()) &&\n        Array.from(element.children).every(check);\n    };\n\n    return Array.from(wrapper.children).every(check);\n  }\n\n  /**\n   * Find and return all block elements in the passed parent (including subtree)\n   *\n   * @param {HTMLElement} parent - root element\n   * @returns {HTMLElement[]}\n   */\n  public static getDeepestBlockElements(parent: HTMLElement): HTMLElement[] {\n    if (Dom.containsOnlyInlineElements(parent)) {\n      return [ parent ];\n    }\n\n    return Array.from(parent.children).reduce((result, element) => {\n      return [...result, ...Dom.getDeepestBlockElements(element as HTMLElement)];\n    }, []);\n  }\n\n  /**\n   * Helper for get holder from {string} or return HTMLElement\n   *\n   * @param {string | HTMLElement} element - holder's id or holder's HTML Element\n   * @returns {HTMLElement}\n   */\n  public static getHolder(element: string | HTMLElement): HTMLElement {\n    if (_.isString(element)) {\n      return document.getElementById(element);\n    }\n\n    return element;\n  }\n\n  /**\n   * Returns true if element is anchor (is A tag)\n   *\n   * @param {Element} element - element to check\n   * @returns {boolean}\n   */\n  public static isAnchor(element: Element): element is HTMLAnchorElement {\n    return element.tagName.toLowerCase() === 'a';\n  }\n\n  /**\n   * Returns the closest ancestor anchor (A tag) of the given element (including itself)\n   * \n   * @param element - element to check\n   * @returns {HTMLAnchorElement | null}\n   */\n  public static getClosestAnchor(element: Element): HTMLAnchorElement | null {\n    return element.closest(\"a\");\n  }\n\n  /**\n   * Return element's offset related to the document\n   *\n   * @todo handle case when editor initialized in scrollable popup\n   * @param el - element to compute offset\n   */\n  public static offset(el): { top: number; left: number; right: number; bottom: number } {\n    const rect = el.getBoundingClientRect();\n    const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;\n    const scrollTop = window.pageYOffset || document.documentElement.scrollTop;\n\n    const top = rect.top + scrollTop;\n    const left = rect.left + scrollLeft;\n\n    return {\n      top,\n      left,\n      bottom: top + rect.height,\n      right: left + rect.width,\n    };\n  }\n\n  /**\n   * Find text node and offset by total content offset\n   *\n   * @param {Node} root - root node to start search from\n   * @param {number} totalOffset - offset relative to the root node content\n   * @returns {{node: Node | null, offset: number}} - node and offset inside node\n   */\n  public static getNodeByOffset(root: Node, totalOffset: number): {node: Node | null; offset: number} {\n    let currentOffset = 0;\n    let lastTextNode: Node | null = null;\n\n    const walker = document.createTreeWalker(\n      root,\n      NodeFilter.SHOW_TEXT,\n      null\n    );\n\n    let node: Node | null = walker.nextNode();\n\n    while (node) {\n      const textContent = node.textContent;\n      const nodeLength = textContent === null ? 0 : textContent.length;\n\n      lastTextNode = node;\n\n      if (currentOffset + nodeLength >= totalOffset) {\n        break;\n      }\n\n      currentOffset += nodeLength;\n      node = walker.nextNode();\n    }\n\n    /**\n     * If no node found or last node is empty, return null\n     * - The root node has no text nodes at all\n     * - The TreeWalker couldn't find any text nodes in the DOM tree\n     * - The root node itself is null or invalid\n     */\n    if (!lastTextNode) {\n      return {\n        node: null,\n        offset: 0,\n      };\n    }\n\n    const textContent = lastTextNode.textContent;\n\n    /**\n     * - The text node exists but has no content (textContent is null)\n     * - The text node exists but has empty content (textContent.length === 0)\n     * This could be due to:\n     * - Empty text nodes (<span></span>)\n     * - Nodes with only whitespace\n     * - Nodes that were cleared but not removed\n     */\n    if (textContent === null || textContent.length === 0) {\n      return {\n        node: null,\n        offset: 0,\n      };\n    }\n\n    /**\n     * Calculate offset inside found node\n     */\n    const nodeOffset = Math.min(totalOffset - currentOffset, textContent.length);\n\n    return {\n      node: lastTextNode,\n      offset: nodeOffset,\n    };\n  }\n}\n\n/**\n * Determine whether a passed text content is a collapsed whitespace.\n *\n * In HTML, whitespaces at the start and end of elements and outside elements are ignored.\n * There are two types of whitespaces in HTML:\n * - Visible (&nbsp;)\n * - Invisible (regular trailing spaces, tabs, etc)\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace\n * @see https://www.w3.org/TR/css-text-3/#white-space-processing\n * @param textContent — any string, for ex a textContent of a node\n * @returns True if passed text content is whitespace which is collapsed (invisible) in browser\n */\nexport function isCollapsedWhitespaces(textContent: string): boolean {\n  /**\n   *  Throughout, whitespace is defined as one of the characters\n   *  \"\\t\" TAB \\u0009\n   *  \"\\n\" LF  \\u000A\n   *  \"\\r\" CR  \\u000D\n   *  \" \"  SPC \\u0020\n   */\n  return !/[^\\t\\n\\r ]/.test(textContent);\n}\n\n/**\n * Calculates the Y coordinate of the text baseline from the top of the element's margin box,\n *\n * The calculation formula is as follows:\n *\n * 1. Calculate the baseline offset:\n *    - Typically, the baseline is about 80% of the `fontSize` from the top of the text, as this is a common average for many fonts.\n *\n * 2. Calculate the additional space due to `lineHeight`:\n *    - If the `lineHeight` is greater than the `fontSize`, the extra space is evenly distributed above and below the text. This extra space is `(lineHeight - fontSize) / 2`.\n *\n * 3. Calculate the total baseline Y coordinate:\n *    - Sum of `marginTop`, `borderTopWidth`, `paddingTop`, the extra space due to `lineHeight`, and the baseline offset.\n *\n * @param element - The element to calculate the baseline for.\n * @returns {number} - The Y coordinate of the text baseline from the top of the element's margin box.\n */\nexport function calculateBaseline(element: Element): number {\n  const style = window.getComputedStyle(element);\n  const fontSize = parseFloat(style.fontSize);\n  // eslint-disable-next-line @typescript-eslint/no-magic-numbers\n  const lineHeight = parseFloat(style.lineHeight) || fontSize * 1.2; // default line-height if not set\n  const paddingTop = parseFloat(style.paddingTop);\n  const borderTopWidth = parseFloat(style.borderTopWidth);\n  const marginTop = parseFloat(style.marginTop);\n\n  /**\n   * Typically, the baseline is about 80% of the `fontSize` from the top of the text, as this is a common average for many fonts.\n   */\n  // eslint-disable-next-line @typescript-eslint/no-magic-numbers\n  const baselineOffset = fontSize * 0.8;\n\n  /**\n   * If the `lineHeight` is greater than the `fontSize`, the extra space is evenly distributed above and below the text. This extra space is `(lineHeight - fontSize) / 2`.\n   */\n  const extraLineHeight = (lineHeight - fontSize) / 2;\n\n  /**\n   * Calculate the total baseline Y coordinate from the top of the margin box\n   */\n  const baselineY = marginTop + borderTopWidth + paddingTop + extraLineHeight + baselineOffset;\n\n  return baselineY;\n}\n\n/**\n * Toggles the [data-empty] attribute on element depending on its emptiness\n * Used to mark empty inputs with a special attribute for placeholders feature\n *\n * @param element - The element to toggle the [data-empty] attribute on\n */\nexport function toggleEmptyMark(element: HTMLElement): void {\n  element.dataset.empty = Dom.isEmpty(element) ? 'true' : 'false';\n}\n"
  },
  {
    "path": "src/components/domIterator.ts",
    "content": "import Dom from './dom';\nimport * as _ from './utils';\nimport SelectionUtils from './selection';\n\n/**\n * Iterator above passed Elements list.\n * Each next or previous action adds provides CSS-class and sets cursor to this item\n */\nexport default class DomIterator {\n  /**\n   * This is a static property that defines iteration directions\n   *\n   * @type {{RIGHT: string, LEFT: string}}\n   */\n  public static directions = {\n    RIGHT: 'right',\n    LEFT: 'left',\n  };\n\n  /**\n   * User-provided CSS-class name for focused button\n   */\n  private focusedCssClass: string;\n\n  /**\n   * Focused button index.\n   * Default is -1 which means nothing is active\n   *\n   * @type {number}\n   */\n  private cursor = -1;\n\n  /**\n   * Items to flip\n   */\n  private items: HTMLElement[] = [];\n\n  /**\n   * @param {HTMLElement[]} nodeList — the list of iterable HTML-items\n   * @param {string} focusedCssClass - user-provided CSS-class that will be set in flipping process\n   */\n  constructor(\n    nodeList: HTMLElement[],\n    focusedCssClass: string\n  ) {\n    this.items = nodeList || [];\n    this.focusedCssClass = focusedCssClass;\n  }\n\n  /**\n   * Returns Focused button Node\n   *\n   * @returns {HTMLElement}\n   */\n  public get currentItem(): HTMLElement {\n    if (this.cursor === -1) {\n      return null;\n    }\n\n    return this.items[this.cursor];\n  }\n\n  /**\n   * Sets cursor to specified position\n   *\n   * @param cursorPosition - new cursor position\n   */\n  public setCursor(cursorPosition: number): void {\n    if (cursorPosition < this.items.length && cursorPosition >= -1) {\n      this.dropCursor();\n      this.cursor = cursorPosition;\n      this.items[this.cursor].classList.add(this.focusedCssClass);\n    }\n  }\n\n  /**\n   * Sets items. Can be used when iterable items changed dynamically\n   *\n   * @param {HTMLElement[]} nodeList - nodes to iterate\n   */\n  public setItems(nodeList: HTMLElement[]): void {\n    this.items = nodeList;\n  }\n\n  /**\n   * Sets cursor next to the current\n   */\n  public next(): void {\n    this.cursor = this.leafNodesAndReturnIndex(DomIterator.directions.RIGHT);\n  }\n\n  /**\n   * Sets cursor before current\n   */\n  public previous(): void {\n    this.cursor = this.leafNodesAndReturnIndex(DomIterator.directions.LEFT);\n  }\n\n  /**\n   * Sets cursor to the default position and removes CSS-class from previously focused item\n   */\n  public dropCursor(): void {\n    if (this.cursor === -1) {\n      return;\n    }\n\n    this.items[this.cursor].classList.remove(this.focusedCssClass);\n    this.cursor = -1;\n  }\n\n  /**\n   * Leafs nodes inside the target list from active element\n   *\n   * @param {string} direction - leaf direction. Can be 'left' or 'right'\n   * @returns {number} index of focused node\n   */\n  private leafNodesAndReturnIndex(direction: string): number {\n    /**\n     * if items are empty then there is nothing to leaf\n     */\n    if (this.items.length === 0) {\n      return this.cursor;\n    }\n\n    let focusedButtonIndex = this.cursor;\n\n    /**\n     * If activeButtonIndex === -1 then we have no chosen Tool in Toolbox\n     */\n    if (focusedButtonIndex === -1) {\n      /**\n       * Normalize \"previous\" Tool index depending on direction.\n       * We need to do this to highlight \"first\" Tool correctly\n       *\n       * Order of Tools: [0] [1] ... [n - 1]\n       *   [0 = n] because of: n % n = 0 % n\n       *\n       * Direction 'right': for [0] the [n - 1] is a previous index\n       *   [n - 1] -> [0]\n       *\n       * Direction 'left': for [n - 1] the [0] is a previous index\n       *   [n - 1] <- [0]\n       *\n       * @type {number}\n       */\n      focusedButtonIndex = direction === DomIterator.directions.RIGHT ? -1 : 0;\n    } else {\n      /**\n       * If we have chosen Tool then remove highlighting\n       */\n      this.items[focusedButtonIndex].classList.remove(this.focusedCssClass);\n    }\n\n    /**\n     * Count index for next Tool\n     */\n    if (direction === DomIterator.directions.RIGHT) {\n      /**\n       * If we go right then choose next (+1) Tool\n       *\n       * @type {number}\n       */\n      focusedButtonIndex = (focusedButtonIndex + 1) % this.items.length;\n    } else {\n      /**\n       * If we go left then choose previous (-1) Tool\n       * Before counting module we need to add length before because of \"The JavaScript Modulo Bug\"\n       *\n       * @type {number}\n       */\n      focusedButtonIndex = (this.items.length + focusedButtonIndex - 1) % this.items.length;\n    }\n\n    if (Dom.canSetCaret(this.items[focusedButtonIndex])) {\n      /**\n       * Focus input with micro-delay to ensure DOM is updated\n       */\n      // eslint-disable-next-line @typescript-eslint/no-magic-numbers\n      _.delay(() => SelectionUtils.setCursor(this.items[focusedButtonIndex]), 50)();\n    }\n\n    /**\n     * Highlight new chosen Tool\n     */\n    this.items[focusedButtonIndex].classList.add(this.focusedCssClass);\n\n    /**\n     * Return focused button's index\n     */\n    return focusedButtonIndex;\n  }\n}\n"
  },
  {
    "path": "src/components/errors/critical.ts",
    "content": "/**\n * This type of exception will destroy the Editor! Be careful when using it\n */\nexport class CriticalError extends Error {\n}\n"
  },
  {
    "path": "src/components/events/BlockChanged.ts",
    "content": "import type { BlockMutationEvent } from '../../../types/events/block';\n\n/**\n * Fired when some block state has changed\n */\nexport const BlockChanged = 'block changed';\n\n/**\n * Payload that will be passed with the event\n */\nexport interface BlockChangedPayload {\n  /**\n   * CustomEvent describing a block change\n   */\n  event: BlockMutationEvent;\n}\n"
  },
  {
    "path": "src/components/events/BlockHovered.ts",
    "content": "import type Block from '../block';\n\n/**\n * Fired when some block is hovered by user\n */\nexport const BlockHovered = 'block hovered';\n\n/**\n * Payload that will be passed with the event\n */\nexport interface BlockHoveredPayload {\n  /**\n   * Hovered block\n   */\n  block: Block;\n}\n"
  },
  {
    "path": "src/components/events/EditorMobileLayoutToggled.ts",
    "content": "/**\n * Fired when editor mobile layout toggled\n */\nexport const EditorMobileLayoutToggled = 'editor mobile layout toggled';\n\n/**\n * Payload that will be passed with the event\n */\nexport interface EditorMobileLayoutToggledPayload {\n  /**\n   * True, if mobile layout enabled\n   */\n  isEnabled: boolean;\n}\n\n"
  },
  {
    "path": "src/components/events/FakeCursorAboutToBeToggled.ts",
    "content": "/**\n * Fired before we're adding/removing a fake cursor.\n *\n * Allows to disable mutation observer to skip this block change\n */\nexport const FakeCursorAboutToBeToggled = 'fake cursor is about to be toggled';\n\n/**\n * Payload that will be passed with the event\n */\nexport interface FakeCursorAboutToBeToggledPayload {\n  /**\n   * true - when added a cursor\n   * false - when removed\n   */\n  state: boolean;\n}\n"
  },
  {
    "path": "src/components/events/FakeCursorHaveBeenSet.ts",
    "content": "/**\n * Fired after we've added/removed a fake cursor.\n *\n * Allows to enable mutation observer which was disabled before setting\n */\nexport const FakeCursorHaveBeenSet = 'fake cursor have been set';\n\n/**\n * Payload that will be passed with the event\n */\nexport interface FakeCursorHaveBeenSetPayload {\n  /**\n   * true - when added a cursor\n   * false - when removed\n   */\n  state: boolean;\n}\n"
  },
  {
    "path": "src/components/events/RedactorDomChanged.ts",
    "content": "/**\n * Fired when blocks wrapper (.codex-editor-redactor) dom changed\n */\nexport const RedactorDomChanged = 'redactor dom changed';\n\n/**\n * Payload that will be passed with the event\n */\nexport interface RedactorDomChangedPayload {\n  /**\n   * Mutations happened with blocks wrapper\n   */\n  mutations: MutationRecord[];\n}\n"
  },
  {
    "path": "src/components/events/index.ts",
    "content": "import type { RedactorDomChangedPayload } from './RedactorDomChanged';\nimport { RedactorDomChanged } from './RedactorDomChanged';\nimport type { BlockChangedPayload } from './BlockChanged';\nimport { BlockChanged } from './BlockChanged';\nimport type { BlockHovered, BlockHoveredPayload } from './BlockHovered';\nimport type { FakeCursorAboutToBeToggledPayload } from './FakeCursorAboutToBeToggled';\nimport { FakeCursorAboutToBeToggled } from './FakeCursorAboutToBeToggled';\nimport type { FakeCursorHaveBeenSetPayload } from './FakeCursorHaveBeenSet';\nimport { FakeCursorHaveBeenSet } from './FakeCursorHaveBeenSet';\nimport type { EditorMobileLayoutToggledPayload } from './EditorMobileLayoutToggled';\nimport { EditorMobileLayoutToggled } from './EditorMobileLayoutToggled';\n\n/**\n * Events fired by Editor Event Dispatcher\n */\nexport {\n  RedactorDomChanged,\n  BlockChanged,\n  FakeCursorAboutToBeToggled,\n  FakeCursorHaveBeenSet,\n  EditorMobileLayoutToggled\n};\n\n/**\n * Event name -> Event payload\n */\nexport interface EditorEventMap {\n  [BlockHovered]: BlockHoveredPayload;\n  [RedactorDomChanged]: RedactorDomChangedPayload;\n  [BlockChanged]: BlockChangedPayload;\n  [FakeCursorAboutToBeToggled]: FakeCursorAboutToBeToggledPayload;\n  [FakeCursorHaveBeenSet]: FakeCursorHaveBeenSetPayload;\n  [EditorMobileLayoutToggled]: EditorMobileLayoutToggledPayload\n}\n"
  },
  {
    "path": "src/components/flipper.ts",
    "content": "import DomIterator from './domIterator';\nimport * as _ from './utils';\n\n/**\n * Flipper construction options\n *\n * @interface FlipperOptions\n */\nexport interface FlipperOptions {\n  /**\n   * CSS-modifier for focused item\n   */\n  focusedItemClass?: string;\n\n  /**\n   * If flipping items are the same for all Block (for ex. Toolbox), ypu can pass it on constructing\n   */\n  items?: HTMLElement[];\n\n  /**\n   * Optional callback for button click\n   */\n  activateCallback?: (item: HTMLElement) => void;\n\n  /**\n   * List of keys allowed for handling.\n   * Can include codes of the following keys:\n   *  - Tab\n   *  - Enter\n   *  - Arrow up\n   *  - Arrow down\n   *  - Arrow right\n   *  - Arrow left\n   * If not specified all keys are enabled\n   */\n  allowedKeys?: number[];\n}\n\n/**\n * Flipper is a component that iterates passed items array by TAB or Arrows and clicks it by ENTER\n */\nexport default class Flipper {\n  /**\n   * True if flipper is currently activated\n   */\n  public get isActivated(): boolean {\n    return this.activated;\n  }\n\n  /**\n   * Instance of flipper iterator\n   */\n  private readonly iterator: DomIterator | null = null;\n\n  /**\n   * Flag that defines activation status\n   */\n  private activated = false;\n\n  /**\n   * List codes of the keys allowed for handling\n   */\n  private readonly allowedKeys: number[];\n\n  /**\n   * Call back for button click/enter\n   */\n  private readonly activateCallback: (item: HTMLElement) => void;\n\n  /**\n   * Contains list of callbacks to be executed on each flip\n   */\n  private flipCallbacks: Array<() => void> = [];\n\n  /**\n   * @param options - different constructing settings\n   */\n  constructor(options: FlipperOptions) {\n    this.iterator = new DomIterator(options.items, options.focusedItemClass);\n    this.activateCallback = options.activateCallback;\n    this.allowedKeys = options.allowedKeys || Flipper.usedKeys;\n  }\n\n  /**\n   * Array of keys (codes) that is handled by Flipper\n   * Used to:\n   *  - preventDefault only for this keys, not all keydowns (@see constructor)\n   *  - to skip external behaviours only for these keys, when filler is activated (@see BlockEvents@arrowRightAndDown)\n   */\n  public static get usedKeys(): number[] {\n    return [\n      _.keyCodes.TAB,\n      _.keyCodes.LEFT,\n      _.keyCodes.RIGHT,\n      _.keyCodes.ENTER,\n      _.keyCodes.UP,\n      _.keyCodes.DOWN,\n    ];\n  }\n\n  /**\n   * Active tab/arrows handling by flipper\n   *\n   * @param items - Some modules (like, InlineToolbar, BlockSettings) might refresh buttons dynamically\n   * @param cursorPosition - index of the item that should be focused once flipper is activated\n   */\n  public activate(items?: HTMLElement[], cursorPosition?: number): void {\n    this.activated = true;\n    if (items) {\n      this.iterator.setItems(items);\n    }\n\n    if (cursorPosition !== undefined) {\n      this.iterator.setCursor(cursorPosition);\n    }\n\n    /**\n     * Listening all keydowns on document and react on TAB/Enter press\n     * TAB will leaf iterator items\n     * ENTER will click the focused item\n     *\n     * Note: the event should be handled in capturing mode on following reasons:\n     * - prevents plugins inner keydown handlers from being called while keyboard navigation\n     * - otherwise this handler will be called at the moment it is attached which causes false flipper firing (see https://techread.me/js-addeventlistener-fires-for-past-events/)\n     */\n    document.addEventListener('keydown', this.onKeyDown, true);\n  }\n\n  /**\n   * Disable tab/arrows handling by flipper\n   */\n  public deactivate(): void {\n    this.activated = false;\n    this.dropCursor();\n\n    document.removeEventListener('keydown', this.onKeyDown);\n  }\n\n  /**\n   * Focus first item\n   */\n  public focusFirst(): void {\n    this.dropCursor();\n    this.flipRight();\n  }\n\n  /**\n   * Focuses previous flipper iterator item\n   */\n  public flipLeft(): void {\n    this.iterator.previous();\n    this.flipCallback();\n  }\n\n  /**\n   * Focuses next flipper iterator item\n   */\n  public flipRight(): void {\n    this.iterator.next();\n    this.flipCallback();\n  }\n\n  /**\n   * Return true if some button is focused\n   */\n  public hasFocus(): boolean {\n    return !!this.iterator.currentItem;\n  }\n\n  /**\n   * Registeres function that should be executed on each navigation action\n   *\n   * @param cb - function to execute\n   */\n  public onFlip(cb: () => void): void {\n    this.flipCallbacks.push(cb);\n  }\n\n  /**\n   * Unregisteres function that is executed on each navigation action\n   *\n   * @param cb - function to stop executing\n   */\n  public removeOnFlip(cb: () => void): void {\n    this.flipCallbacks = this.flipCallbacks.filter(fn => fn !== cb);\n  }\n\n  /**\n   * Drops flipper's iterator cursor\n   *\n   * @see DomIterator#dropCursor\n   */\n  private dropCursor(): void {\n    this.iterator.dropCursor();\n  }\n\n  /**\n   * KeyDown event handler\n   *\n   * @param event - keydown event\n   */\n  private onKeyDown = (event: KeyboardEvent): void => {\n    const isReady = this.isEventReadyForHandling(event);\n\n    if (!isReady) {\n      return;\n    }\n\n    const isShiftKey = event.shiftKey;\n\n    /**\n     * If shift key is pressed, do nothing\n     * Allows to select next/prev lines of text using keyboard\n     */\n    if (isShiftKey === true) {\n      return;\n    }\n\n    /**\n     * Prevent only used keys default behaviour\n     * (allows to navigate by ARROW DOWN, for example)\n     */\n    if (Flipper.usedKeys.includes(event.keyCode)) {\n      event.preventDefault();\n    }\n\n    switch (event.keyCode) {\n      case _.keyCodes.TAB:\n        this.handleTabPress(event);\n        break;\n      case _.keyCodes.LEFT:\n      case _.keyCodes.UP:\n        this.flipLeft();\n        break;\n      case _.keyCodes.RIGHT:\n      case _.keyCodes.DOWN:\n        this.flipRight();\n        break;\n      case _.keyCodes.ENTER:\n        this.handleEnterPress(event);\n        break;\n    }\n  };\n\n  /**\n   * This function is fired before handling flipper keycodes\n   * The result of this function defines if it is need to be handled or not\n   *\n   * @param {KeyboardEvent} event - keydown keyboard event\n   * @returns {boolean}\n   */\n  private isEventReadyForHandling(event: KeyboardEvent): boolean {\n    return this.activated && this.allowedKeys.includes(event.keyCode);\n  }\n\n  /**\n   * When flipper is activated tab press will leaf the items\n   *\n   * @param {KeyboardEvent} event - tab keydown event\n   */\n  private handleTabPress(event: KeyboardEvent): void {\n    /** this property defines leaf direction */\n    const shiftKey = event.shiftKey,\n        direction = shiftKey ? DomIterator.directions.LEFT : DomIterator.directions.RIGHT;\n\n    switch (direction) {\n      case DomIterator.directions.RIGHT:\n        this.flipRight();\n        break;\n      case DomIterator.directions.LEFT:\n        this.flipLeft();\n        break;\n    }\n  }\n\n  /**\n   * Enter press will click current item if flipper is activated\n   *\n   * @param {KeyboardEvent} event - enter keydown event\n   */\n  private handleEnterPress(event: KeyboardEvent): void {\n    if (!this.activated) {\n      return;\n    }\n\n    if (this.iterator.currentItem) {\n      /**\n       * Stop Enter propagation only if flipper is ready to select focused item\n       */\n      event.stopPropagation();\n      event.preventDefault();\n      this.iterator.currentItem.click();\n    }\n\n    if (_.isFunction(this.activateCallback)) {\n      this.activateCallback(this.iterator.currentItem);\n    }\n  }\n\n  /**\n   * Fired after flipping in any direction\n   */\n  private flipCallback(): void {\n    if (this.iterator.currentItem) {\n      this.iterator.currentItem.scrollIntoViewIfNeeded();\n    }\n\n    this.flipCallbacks.forEach(cb => cb());\n  }\n}\n"
  },
  {
    "path": "src/components/i18n/index.ts",
    "content": "import defaultDictionary from './locales/en/messages.json';\nimport type { I18nDictionary, Dictionary } from '../../../types/configs';\nimport type { LeavesDictKeys } from '../../types-internal/i18n-internal-namespace';\n\n/**\n * Type for all available internal dictionary strings\n */\ntype DictKeys = LeavesDictKeys<typeof defaultDictionary>;\n\n/**\n * This class will responsible for the translation through the language dictionary\n */\nexport default class I18n {\n  /**\n   * Property that stores messages dictionary\n   */\n  private static currentDictionary: I18nDictionary = defaultDictionary;\n\n  /**\n   * Type-safe translation for internal UI texts:\n   * Perform translation of the string by namespace and a key\n   *\n   * @example I18n.ui(I18nInternalNS.ui.blockTunes.toggler, 'Click to tune')\n   * @param internalNamespace - path to translated string in dictionary\n   * @param dictKey - dictionary key. Better to use default locale original text\n   */\n  public static ui(internalNamespace: string, dictKey: DictKeys): string {\n    return I18n._t(internalNamespace, dictKey);\n  }\n\n  /**\n   * Translate for external strings that is not presented in default dictionary.\n   * For example, for user-specified tool names\n   *\n   * @param namespace - path to translated string in dictionary\n   * @param dictKey - dictionary key. Better to use default locale original text\n   */\n  public static t(namespace: string, dictKey: string): string {\n    return I18n._t(namespace, dictKey);\n  }\n\n  /**\n   * Adjust module for using external dictionary\n   *\n   * @param dictionary - new messages list to override default\n   */\n  public static setDictionary(dictionary: I18nDictionary): void {\n    I18n.currentDictionary = dictionary;\n  }\n\n  /**\n   * Perform translation both for internal and external namespaces\n   * If there is no translation found, returns passed key as a translated message\n   *\n   * @param namespace - path to translated string in dictionary\n   * @param dictKey - dictionary key. Better to use default locale original text\n   */\n  private static _t(namespace: string, dictKey: string): string {\n    const section = I18n.getNamespace(namespace);\n\n    /**\n     * For Console Message to Check Section is defined or not\n     * if (section === undefined) {\n     *  _.logLabeled('I18n: section %o was not found in current dictionary', 'log', namespace);\n     * }\n     */\n\n    if (!section || !section[dictKey]) {\n      return dictKey;\n    }\n\n    return section[dictKey] as string;\n  }\n\n  /**\n   * Find messages section by namespace path\n   *\n   * @param namespace - path to section\n   */\n  private static getNamespace(namespace: string): Dictionary {\n    const parts = namespace.split('.');\n\n    return parts.reduce((section, part) => {\n      if (!section || !Object.keys(section).length) {\n        return {};\n      }\n\n      return section[part];\n    }, I18n.currentDictionary);\n  }\n}\n"
  },
  {
    "path": "src/components/i18n/locales/en/messages.json",
    "content": "{\n  \"ui\": {\n    \"blockTunes\": {\n      \"toggler\": {\n        \"Click to tune\": \"\",\n        \"or drag to move\": \"\"\n      }\n    },\n    \"inlineToolbar\": {\n      \"converter\": {\n        \"Convert to\": \"\"\n      }\n    },\n    \"toolbar\": {\n      \"toolbox\": {\n        \"Add\": \"\"\n      }\n    },\n    \"popover\": {\n      \"Filter\": \"\",\n      \"Nothing found\": \"\",\n      \"Convert to\": \"\"\n    }\n  },\n  \"toolNames\": {\n    \"Text\": \"\",\n    \"Link\": \"\",\n    \"Bold\": \"\",\n    \"Italic\": \"\"\n  },\n  \"tools\": {\n    \"link\": {\n      \"Add a link\": \"\"\n    },\n    \"stub\": {\n      \"The block can not be displayed correctly.\": \"\"\n    }\n  },\n  \"blockTunes\": {\n    \"delete\": {\n      \"Delete\": \"\",\n      \"Click to delete\": \"\"\n    },\n    \"moveUp\": {\n      \"Move up\": \"\"\n    },\n    \"moveDown\": {\n      \"Move down\": \"\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/i18n/namespace-internal.ts",
    "content": "import defaultDictionary from './locales/en/messages.json';\nimport type { DictNamespaces } from '../../types-internal/i18n-internal-namespace';\nimport { isObject, isString } from '../utils';\n\n/**\n * Evaluate messages dictionary and return object for namespace chaining\n *\n * @param dict - Messages dictionary\n * @param [keyPath] - subsection path (used in recursive call)\n */\nfunction getNamespaces(dict: object, keyPath?: string): DictNamespaces<typeof defaultDictionary> {\n  const result = {};\n\n  Object.entries(dict).forEach(([key, section]) => {\n    if (isObject(section)) {\n      const newPath = keyPath ? `${keyPath}.${key}` : key;\n\n      /**\n       * Check current section values, if all of them are strings, so there is the last section\n       */\n      const isLastSection = Object.values(section).every((sectionValue) => {\n        return isString(sectionValue);\n      });\n\n      /**\n       * In last section, we substitute namespace path instead of object with translates\n       *\n       * ui.toolbar.toolbox – \"ui.toolbar.toolbox\"\n       * instead of\n       * ui.toolbar.toolbox – {\"Add\": \"\"}\n       */\n      if (isLastSection) {\n        result[key] = newPath;\n      } else {\n        result[key] = getNamespaces(section, newPath);\n      }\n\n      return;\n    }\n\n    result[key] = section;\n  });\n\n  return result as DictNamespaces<typeof defaultDictionary>;\n}\n\n/**\n * Type safe access to the internal messages dictionary sections\n *\n * @example I18n.ui(I18nInternalNS.ui.blockTunes.toggler, 'Click to tune');\n */\nexport const I18nInternalNS = getNamespaces(defaultDictionary);\n"
  },
  {
    "path": "src/components/inline-tools/inline-tool-bold.ts",
    "content": "import type { InlineTool, SanitizerConfig } from '../../../types';\nimport { IconBold } from '@codexteam/icons';\nimport type { MenuConfig } from '../../../types/tools';\n\n/**\n * Bold Tool\n *\n * Inline Toolbar Tool\n *\n * Makes selected text bolder\n */\nexport default class BoldInlineTool implements InlineTool {\n  /**\n   * Specifies Tool as Inline Toolbar Tool\n   *\n   * @returns {boolean}\n   */\n  public static isInline = true;\n\n  /**\n   * Title for hover-tooltip\n   */\n  public static title = 'Bold';\n\n  /**\n   * Sanitizer Rule\n   * Leave <b> tags\n   *\n   * @returns {object}\n   */\n  public static get sanitize(): SanitizerConfig {\n    return {\n      b: {},\n    } as SanitizerConfig;\n  }\n\n  /**\n   * Native Document's command that uses for Bold\n   */\n  private readonly commandName: string = 'bold';\n\n  /**\n   * Create button for Inline Toolbar\n   */\n  public render(): MenuConfig {\n    return {\n      icon: IconBold,\n      name: 'bold',\n      onActivate: () => {\n        document.execCommand(this.commandName);\n      },\n      isActive: () => document.queryCommandState(this.commandName),\n    };\n  }\n\n  /**\n   * Set a shortcut\n   *\n   * @returns {boolean}\n   */\n  public get shortcut(): string {\n    return 'CMD+B';\n  }\n}\n"
  },
  {
    "path": "src/components/inline-tools/inline-tool-convert.ts",
    "content": "import { IconReplace } from '@codexteam/icons';\nimport type { InlineTool, API } from '../../../types';\nimport type { MenuConfig, MenuConfigItem } from '../../../types/tools';\nimport * as _ from '../utils';\nimport type { Blocks, Selection, Tools, Caret, I18n } from '../../../types/api';\nimport SelectionUtils from '../selection';\nimport { getConvertibleToolsForBlock } from '../utils/blocks';\nimport I18nInternal from '../i18n';\nimport { I18nInternalNS } from '../i18n/namespace-internal';\n\n/**\n * Inline tools for converting blocks\n */\nexport default class ConvertInlineTool implements InlineTool {\n  /**\n   * Specifies Tool as Inline Toolbar Tool\n   */\n  public static isInline = true;\n\n  /**\n   * API for working with editor blocks\n   */\n  private readonly blocksAPI: Blocks;\n\n  /**\n   * API for working with Selection\n   */\n  private readonly selectionAPI: Selection;\n\n  /**\n   * API for working with Tools\n   */\n  private readonly toolsAPI: Tools;\n\n  /**\n   * I18n API\n   */\n  private readonly i18nAPI: I18n;\n\n  /**\n   * API for working with Caret\n   */\n  private readonly caretAPI: Caret;\n\n  /**\n   * @param api - Editor.js API\n   */\n  constructor({ api }: { api: API }) {\n    this.i18nAPI = api.i18n;\n    this.blocksAPI = api.blocks;\n    this.selectionAPI = api.selection;\n    this.toolsAPI = api.tools;\n    this.caretAPI = api.caret;\n  }\n\n  /**\n   * Returns tool's UI config\n   */\n  public async render(): Promise<MenuConfig> {\n    const currentSelection = SelectionUtils.get();\n    const currentBlock = this.blocksAPI.getBlockByElement(currentSelection.anchorNode as HTMLElement);\n\n    if (currentBlock === undefined) {\n      return [];\n    }\n\n    const allBlockTools = this.toolsAPI.getBlockTools();\n    const convertibleTools = await getConvertibleToolsForBlock(currentBlock, allBlockTools);\n\n    if (convertibleTools.length === 0) {\n      return [];\n    }\n\n    const convertToItems = convertibleTools.reduce<MenuConfigItem[]>((result, tool) => {\n      tool.toolbox?.forEach((toolboxItem) => {\n        result.push({\n          icon: toolboxItem.icon,\n          title: I18nInternal.t(I18nInternalNS.toolNames, toolboxItem.title),\n          name: tool.name,\n          closeOnActivate: true,\n          onActivate: async () => {\n            const newBlock = await this.blocksAPI.convert(currentBlock.id, tool.name, toolboxItem.data);\n\n            this.caretAPI.setToBlock(newBlock, 'end');\n          },\n        });\n      });\n\n      return result;\n    }, []);\n\n    const currentBlockToolboxItem = await currentBlock.getActiveToolboxEntry();\n    const icon = currentBlockToolboxItem !== undefined ? currentBlockToolboxItem.icon : IconReplace;\n    const isDesktop =  !_.isMobileScreen();\n\n    return {\n      icon,\n      name: 'convert-to',\n      hint: {\n        title: this.i18nAPI.t('Convert to'),\n      },\n      children: {\n        searchable: isDesktop,\n        items: convertToItems,\n        onOpen: () => {\n          if (isDesktop) {\n            this.selectionAPI.setFakeBackground();\n            this.selectionAPI.save();\n          }\n        },\n        onClose: () => {\n          if (isDesktop) {\n            this.selectionAPI.restore();\n            this.selectionAPI.removeFakeBackground();\n          }\n        },\n      },\n    };\n  }\n}\n"
  },
  {
    "path": "src/components/inline-tools/inline-tool-italic.ts",
    "content": "import type { InlineTool, SanitizerConfig } from '../../../types';\nimport { IconItalic } from '@codexteam/icons';\n\n/**\n * Italic Tool\n *\n * Inline Toolbar Tool\n *\n * Style selected text with italic\n */\nexport default class ItalicInlineTool implements InlineTool {\n  /**\n   * Specifies Tool as Inline Toolbar Tool\n   *\n   * @returns {boolean}\n   */\n  public static isInline = true;\n\n  /**\n   * Title for hover-tooltip\n   */\n  public static title = 'Italic';\n\n  /**\n   * Sanitizer Rule\n   * Leave <i> tags\n   *\n   * @returns {object}\n   */\n  public static get sanitize(): SanitizerConfig {\n    return {\n      i: {},\n    } as SanitizerConfig;\n  }\n\n  /**\n   * Native Document's command that uses for Italic\n   */\n  private readonly commandName: string = 'italic';\n\n  /**\n   * Styles\n   */\n  private readonly CSS = {\n    button: 'ce-inline-tool',\n    buttonActive: 'ce-inline-tool--active',\n    buttonModifier: 'ce-inline-tool--italic',\n  };\n\n  /**\n   * Elements\n   */\n  private nodes: {button: HTMLButtonElement} = {\n    button: null,\n  };\n\n  /**\n   * Create button for Inline Toolbar\n   */\n  public render(): HTMLElement {\n    this.nodes.button = document.createElement('button') as HTMLButtonElement;\n    this.nodes.button.type = 'button';\n    this.nodes.button.classList.add(this.CSS.button, this.CSS.buttonModifier);\n    this.nodes.button.innerHTML = IconItalic;\n\n    return this.nodes.button;\n  }\n\n  /**\n   * Wrap range with <i> tag\n   */\n  public surround(): void {\n    document.execCommand(this.commandName);\n  }\n\n  /**\n   * Check selection and set activated state to button if there are <i> tag\n   */\n  public checkState(): boolean {\n    const isActive = document.queryCommandState(this.commandName);\n\n    this.nodes.button.classList.toggle(this.CSS.buttonActive, isActive);\n\n    return isActive;\n  }\n\n  /**\n   * Set a shortcut\n   */\n  public get shortcut(): string {\n    return 'CMD+I';\n  }\n}\n"
  },
  {
    "path": "src/components/inline-tools/inline-tool-link.ts",
    "content": "import SelectionUtils from '../selection';\nimport * as _ from '../utils';\nimport type { InlineTool, SanitizerConfig, API } from '../../../types';\nimport type { Notifier, Toolbar, I18n, InlineToolbar } from '../../../types/api';\nimport { IconLink, IconUnlink } from '@codexteam/icons';\n\n/**\n * Link Tool\n *\n * Inline Toolbar Tool\n *\n * Wrap selected text with <a> tag\n */\nexport default class LinkInlineTool implements InlineTool {\n  /**\n   * Specifies Tool as Inline Toolbar Tool\n   *\n   * @returns {boolean}\n   */\n  public static isInline = true;\n\n  /**\n   * Title for hover-tooltip\n   */\n  public static title = 'Link';\n\n  /**\n   * Sanitizer Rule\n   * Leave <a> tags\n   *\n   * @returns {object}\n   */\n  public static get sanitize(): SanitizerConfig {\n    return {\n      a: {\n        href: true,\n        target: '_blank',\n        rel: 'nofollow',\n      },\n    } as SanitizerConfig;\n  }\n\n  /**\n   * Native Document's commands for link/unlink\n   */\n  private readonly commandLink: string = 'createLink';\n  private readonly commandUnlink: string = 'unlink';\n\n  /**\n   * Enter key code\n   */\n  private readonly ENTER_KEY: number = 13;\n\n  /**\n   * Styles\n   */\n  private readonly CSS = {\n    button: 'ce-inline-tool',\n    buttonActive: 'ce-inline-tool--active',\n    buttonModifier: 'ce-inline-tool--link',\n    buttonUnlink: 'ce-inline-tool--unlink',\n    input: 'ce-inline-tool-input',\n    inputShowed: 'ce-inline-tool-input--showed',\n  };\n\n  /**\n   * Elements\n   */\n  private nodes: {\n    button: HTMLButtonElement;\n    input: HTMLInputElement;\n  } = {\n      button: null,\n      input: null,\n    };\n\n  /**\n   * SelectionUtils instance\n   */\n  private selection: SelectionUtils;\n\n  /**\n   * Input opening state\n   */\n  private inputOpened = false;\n\n  /**\n   * Available Toolbar methods (open/close)\n   */\n  private toolbar: Toolbar;\n\n  /**\n   * Available inline toolbar methods (open/close)\n   */\n  private inlineToolbar: InlineToolbar;\n\n  /**\n   * Notifier API methods\n   */\n  private notifier: Notifier;\n\n  /**\n   * I18n API\n   */\n  private i18n: I18n;\n\n  /**\n   * @param api - Editor.js API\n   */\n  constructor({ api }: { api: API }) {\n    this.toolbar = api.toolbar;\n    this.inlineToolbar = api.inlineToolbar;\n    this.notifier = api.notifier;\n    this.i18n = api.i18n;\n    this.selection = new SelectionUtils();\n  }\n\n  /**\n   * Create button for Inline Toolbar\n   */\n  public render(): HTMLElement {\n    this.nodes.button = document.createElement('button') as HTMLButtonElement;\n    this.nodes.button.type = 'button';\n    this.nodes.button.classList.add(this.CSS.button, this.CSS.buttonModifier);\n\n    this.nodes.button.innerHTML = IconLink;\n\n    return this.nodes.button;\n  }\n\n  /**\n   * Input for the link\n   */\n  public renderActions(): HTMLElement {\n    this.nodes.input = document.createElement('input') as HTMLInputElement;\n    this.nodes.input.placeholder = this.i18n.t('Add a link');\n    this.nodes.input.enterKeyHint = 'done';\n    this.nodes.input.classList.add(this.CSS.input);\n    this.nodes.input.addEventListener('keydown', (event: KeyboardEvent) => {\n      if (event.keyCode === this.ENTER_KEY) {\n        this.enterPressed(event);\n      }\n    });\n\n    return this.nodes.input;\n  }\n\n  /**\n   * Handle clicks on the Inline Toolbar icon\n   *\n   * @param {Range} range - range to wrap with link\n   */\n  public surround(range: Range): void {\n    /**\n     * Range will be null when user makes second click on the 'link icon' to close opened input\n     */\n    if (range) {\n      /**\n       * Save selection before change focus to the input\n       */\n      if (!this.inputOpened) {\n        /** Create blue background instead of selection */\n        this.selection.setFakeBackground();\n        this.selection.save();\n      } else {\n        this.selection.restore();\n        this.selection.removeFakeBackground();\n      }\n      const parentAnchor = this.selection.findParentTag('A');\n\n      /**\n       * Unlink icon pressed\n       */\n      if (parentAnchor) {\n        /**\n         * If input is not opened, treat click as explicit unlink action.\n         * If input is opened (e.g., programmatic close when switching tools), avoid unlinking.\n         */\n        if (!this.inputOpened) {\n          this.selection.expandToTag(parentAnchor);\n          this.unlink();\n          this.closeActions();\n          this.checkState();\n          this.toolbar.close();\n        } else {\n          /** Only close actions without clearing saved selection to preserve user state */\n          this.closeActions(false);\n          this.checkState();\n        }\n\n        return;\n      }\n    }\n\n    this.toggleActions();\n  }\n\n  /**\n   * Check selection and set activated state to button if there are <a> tag\n   */\n  public checkState(): boolean {\n    const anchorTag = this.selection.findParentTag('A');\n\n    if (anchorTag) {\n      this.nodes.button.innerHTML = IconUnlink;\n      this.nodes.button.classList.add(this.CSS.buttonUnlink);\n      this.nodes.button.classList.add(this.CSS.buttonActive);\n      this.openActions();\n\n      /**\n       * Fill input value with link href\n       */\n      const hrefAttr = anchorTag.getAttribute('href');\n\n      this.nodes.input.defaultValue = hrefAttr !== 'null' ? hrefAttr : '';\n\n      this.selection.save();\n    } else {\n      this.nodes.button.innerHTML = IconLink;\n      this.nodes.button.classList.remove(this.CSS.buttonUnlink);\n      this.nodes.button.classList.remove(this.CSS.buttonActive);\n    }\n\n    return !!anchorTag;\n  }\n\n  /**\n   * Function called with Inline Toolbar closing\n   */\n  public clear(): void {\n    this.closeActions();\n  }\n\n  /**\n   * Set a shortcut\n   */\n  public get shortcut(): string {\n    return 'CMD+K';\n  }\n\n  /**\n   * Show/close link input\n   */\n  private toggleActions(): void {\n    if (!this.inputOpened) {\n      this.openActions(true);\n    } else {\n      this.closeActions(false);\n    }\n  }\n\n  /**\n   * @param {boolean} needFocus - on link creation we need to focus input. On editing - nope.\n   */\n  private openActions(needFocus = false): void {\n    this.nodes.input.classList.add(this.CSS.inputShowed);\n    if (needFocus) {\n      this.nodes.input.focus();\n    }\n    this.inputOpened = true;\n  }\n\n  /**\n   * Close input\n   *\n   * @param {boolean} clearSavedSelection — we don't need to clear saved selection\n   *                                        on toggle-clicks on the icon of opened Toolbar\n   */\n  private closeActions(clearSavedSelection = true): void {\n    if (this.selection.isFakeBackgroundEnabled) {\n      // if actions is broken by other selection We need to save new selection\n      const currentSelection = new SelectionUtils();\n\n      currentSelection.save();\n\n      this.selection.restore();\n      this.selection.removeFakeBackground();\n\n      // and recover new selection after removing fake background\n      currentSelection.restore();\n    }\n\n    this.nodes.input.classList.remove(this.CSS.inputShowed);\n    this.nodes.input.value = '';\n    if (clearSavedSelection) {\n      this.selection.clearSaved();\n    }\n    this.inputOpened = false;\n  }\n\n  /**\n   * Enter pressed on input\n   *\n   * @param {KeyboardEvent} event - enter keydown event\n   */\n  private enterPressed(event: KeyboardEvent): void {\n    let value = this.nodes.input.value || '';\n\n    if (!value.trim()) {\n      this.selection.restore();\n      this.unlink();\n      event.preventDefault();\n      this.closeActions();\n\n      return;\n    }\n\n    if (!this.validateURL(value)) {\n      this.notifier.show({\n        message: 'Pasted link is not valid.',\n        style: 'error',\n      });\n\n      _.log('Incorrect Link pasted', 'warn', value);\n\n      return;\n    }\n\n    value = this.prepareLink(value);\n\n    this.selection.restore();\n    this.selection.removeFakeBackground();\n\n    this.insertLink(value);\n\n    /**\n     * Preventing events that will be able to happen\n     */\n    event.preventDefault();\n    event.stopPropagation();\n    event.stopImmediatePropagation();\n    this.selection.collapseToEnd();\n    this.inlineToolbar.close();\n  }\n\n  /**\n   * Detects if passed string is URL\n   *\n   * @param {string} str - string to validate\n   * @returns {boolean}\n   */\n  private validateURL(str: string): boolean {\n    /**\n     * Don't allow spaces\n     */\n    return !/\\s/.test(str);\n  }\n\n  /**\n   * Process link before injection\n   * - sanitize\n   * - add protocol for links like 'google.com'\n   *\n   * @param {string} link - raw user input\n   */\n  private prepareLink(link: string): string {\n    link = link.trim();\n    link = this.addProtocol(link);\n\n    return link;\n  }\n\n  /**\n   * Add 'http' protocol to the links like 'vc.ru', 'google.com'\n   *\n   * @param {string} link - string to process\n   */\n  private addProtocol(link: string): string {\n    /**\n     * If protocol already exists, do nothing\n     */\n    if (/^(\\w+):(\\/\\/)?/.test(link)) {\n      return link;\n    }\n\n    /**\n     * We need to add missed HTTP protocol to the link, but skip 2 cases:\n     *     1) Internal links like \"/general\"\n     *     2) Anchors looks like \"#results\"\n     *     3) Protocol-relative URLs like \"//google.com\"\n     */\n    const isInternal = /^\\/[^/\\s]/.test(link),\n        isAnchor = link.substring(0, 1) === '#',\n        isProtocolRelative = /^\\/\\/[^/\\s]/.test(link);\n\n    if (!isInternal && !isAnchor && !isProtocolRelative) {\n      link = 'http://' + link;\n    }\n\n    return link;\n  }\n\n  /**\n   * Inserts <a> tag with \"href\"\n   *\n   * @param {string} link - \"href\" value\n   */\n  private insertLink(link: string): void {\n    /**\n     * Edit all link, not selected part\n     */\n    const anchorTag = this.selection.findParentTag('A');\n\n    if (anchorTag) {\n      this.selection.expandToTag(anchorTag);\n    }\n\n    document.execCommand(this.commandLink, false, link);\n  }\n\n  /**\n   * Removes <a> tag\n   */\n  private unlink(): void {\n    document.execCommand(this.commandUnlink);\n  }\n}\n"
  },
  {
    "path": "src/components/modules/api/blocks.ts",
    "content": "import type { BlockAPI as BlockAPIInterface, Blocks } from '../../../../types/api';\nimport type { BlockToolData, OutputBlockData, OutputData, ToolConfig } from '../../../../types';\nimport * as _ from './../../utils';\nimport BlockAPI from '../../block/api';\nimport Module from '../../__module';\nimport Block from '../../block';\nimport { capitalize } from '../../utils';\nimport type { BlockTuneData } from '../../../../types/block-tunes/block-tune-data';\n\n/**\n * @class BlocksAPI\n * provides with methods working with Block\n */\nexport default class BlocksAPI extends Module {\n  /**\n   * Available methods\n   *\n   * @returns {Blocks}\n   */\n  public get methods(): Blocks {\n    return {\n      clear: (): Promise<void> => this.clear(),\n      render: (data: OutputData): Promise<void> => this.render(data),\n      renderFromHTML: (data: string): Promise<void> => this.renderFromHTML(data),\n      delete: (index?: number): void => this.delete(index),\n      swap: (fromIndex: number, toIndex: number): void => this.swap(fromIndex, toIndex),\n      move: (toIndex: number, fromIndex?: number): void => this.move(toIndex, fromIndex),\n      getBlockByIndex: (index: number): BlockAPIInterface | undefined => this.getBlockByIndex(index),\n      getById: (id: string): BlockAPIInterface | null => this.getById(id),\n      getCurrentBlockIndex: (): number => this.getCurrentBlockIndex(),\n      getBlockIndex: (id: string): number => this.getBlockIndex(id),\n      getBlocksCount: (): number => this.getBlocksCount(),\n      getBlockByElement: (element: HTMLElement) => this.getBlockByElement(element),\n      stretchBlock: (index: number, status = true): void => this.stretchBlock(index, status),\n      insertNewBlock: (): void => this.insertNewBlock(),\n      insert: this.insert,\n      insertMany: this.insertMany,\n      update: this.update,\n      composeBlockData: this.composeBlockData,\n      convert: this.convert,\n    };\n  }\n\n  /**\n   * Returns Blocks count\n   *\n   * @returns {number}\n   */\n  public getBlocksCount(): number {\n    return this.Editor.BlockManager.blocks.length;\n  }\n\n  /**\n   * Returns current block index\n   *\n   * @returns {number}\n   */\n  public getCurrentBlockIndex(): number {\n    return this.Editor.BlockManager.currentBlockIndex;\n  }\n\n  /**\n   * Returns the index of Block by id;\n   *\n   * @param id - block id\n   */\n  public getBlockIndex(id: string): number | undefined {\n    const block = this.Editor.BlockManager.getBlockById(id);\n\n    if (!block) {\n      _.logLabeled('There is no block with id `' + id + '`', 'warn');\n\n      return;\n    }\n\n    return this.Editor.BlockManager.getBlockIndex(block);\n  }\n\n  /**\n   * Returns BlockAPI object by Block index\n   *\n   * @param {number} index - index to get\n   */\n  public getBlockByIndex(index: number): BlockAPIInterface | undefined {\n    const block = this.Editor.BlockManager.getBlockByIndex(index);\n\n    if (block === undefined) {\n      _.logLabeled('There is no block at index `' + index + '`', 'warn');\n\n      return;\n    }\n\n    return new BlockAPI(block);\n  }\n\n  /**\n   * Returns BlockAPI object by Block id\n   *\n   * @param id - id of block to get\n   */\n  public getById(id: string): BlockAPIInterface | null {\n    const block = this.Editor.BlockManager.getBlockById(id);\n\n    if (block === undefined) {\n      _.logLabeled('There is no block with id `' + id + '`', 'warn');\n\n      return null;\n    }\n\n    return new BlockAPI(block);\n  }\n\n  /**\n   * Get Block API object by any child html element\n   *\n   * @param element - html element to get Block by\n   */\n  public getBlockByElement(element: HTMLElement): BlockAPIInterface | undefined {\n    const block = this.Editor.BlockManager.getBlock(element);\n\n    if (block === undefined) {\n      _.logLabeled('There is no block corresponding to element `' + element + '`', 'warn');\n\n      return;\n    }\n\n    return new BlockAPI(block);\n  }\n\n  /**\n   * Call Block Manager method that swap Blocks\n   *\n   * @param {number} fromIndex - position of first Block\n   * @param {number} toIndex - position of second Block\n   * @deprecated — use 'move' instead\n   */\n  public swap(fromIndex: number, toIndex: number): void {\n    _.log(\n      '`blocks.swap()` method is deprecated and will be removed in the next major release. ' +\n      'Use `block.move()` method instead',\n      'info'\n    );\n\n    this.Editor.BlockManager.swap(fromIndex, toIndex);\n  }\n\n  /**\n   * Move block from one index to another\n   *\n   * @param {number} toIndex - index to move to\n   * @param {number} fromIndex - index to move from\n   */\n  public move(toIndex: number, fromIndex?: number): void {\n    this.Editor.BlockManager.move(toIndex, fromIndex);\n  }\n\n  /**\n   * Deletes Block\n   *\n   * @param {number} blockIndex - index of Block to delete\n   */\n  public delete(blockIndex: number = this.Editor.BlockManager.currentBlockIndex): void {\n    try {\n      const block = this.Editor.BlockManager.getBlockByIndex(blockIndex);\n\n      this.Editor.BlockManager.removeBlock(block);\n    } catch (e) {\n      _.logLabeled(e, 'warn');\n\n      return;\n    }\n\n    /**\n     * in case of last block deletion\n     * Insert the new default empty block\n     */\n    if (this.Editor.BlockManager.blocks.length === 0) {\n      this.Editor.BlockManager.insert();\n    }\n\n    /**\n     * After Block deletion currentBlock is updated\n     */\n    if (this.Editor.BlockManager.currentBlock) {\n      this.Editor.Caret.setToBlock(this.Editor.BlockManager.currentBlock, this.Editor.Caret.positions.END);\n    }\n\n    this.Editor.Toolbar.close();\n  }\n\n  /**\n   * Clear Editor's area\n   */\n  public async clear(): Promise<void> {\n    await this.Editor.BlockManager.clear(true);\n    this.Editor.InlineToolbar.close();\n  }\n\n  /**\n   * Fills Editor with Blocks data\n   *\n   * @param {OutputData} data — Saved Editor data\n   */\n  public async render(data: OutputData): Promise<void> {\n    if (data === undefined || data.blocks === undefined) {\n      throw new Error('Incorrect data passed to the render() method');\n    }\n\n    /**\n     * Semantic meaning of the \"render\" method: \"Display the new document over the existing one that stays unchanged\"\n     * So we need to disable modifications observer temporarily\n     */\n    this.Editor.ModificationsObserver.disable();\n\n    await this.Editor.BlockManager.clear();\n    await this.Editor.Renderer.render(data.blocks);\n\n    this.Editor.ModificationsObserver.enable();\n  }\n\n  /**\n   * Render passed HTML string\n   *\n   * @param {string} data - HTML string to render\n   * @returns {Promise<void>}\n   */\n  public async renderFromHTML(data: string): Promise<void> {\n    await this.Editor.BlockManager.clear();\n\n    return this.Editor.Paste.processText(data, true);\n  }\n\n  /**\n   * Stretch Block's content\n   *\n   * @param {number} index - index of Block to stretch\n   * @param {boolean} status - true to enable, false to disable\n   * @deprecated Use BlockAPI interface to stretch Blocks\n   */\n  public stretchBlock(index: number, status = true): void {\n    _.deprecationAssert(\n      true,\n      'blocks.stretchBlock()',\n      'BlockAPI'\n    );\n\n    const block = this.Editor.BlockManager.getBlockByIndex(index);\n\n    if (!block) {\n      return;\n    }\n\n    block.stretched = status;\n  }\n\n  /**\n   * Insert new Block and returns it's API\n   *\n   * @param {string} type — Tool name\n   * @param {BlockToolData} data — Tool data to insert\n   * @param {ToolConfig} config — Tool config\n   * @param {number?} index — index where to insert new Block\n   * @param {boolean?} needToFocus - flag to focus inserted Block\n   * @param replace - pass true to replace the Block existed under passed index\n   * @param {string} id — An optional id for the new block. If omitted then the new id will be generated\n   */\n  public insert = (\n    type: string = this.config.defaultBlock,\n    data: BlockToolData = {},\n    // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars\n    config: ToolConfig = {},\n    index?: number,\n    needToFocus?: boolean,\n    replace?: boolean,\n    id?: string\n  ): BlockAPIInterface => {\n    const insertedBlock = this.Editor.BlockManager.insert({\n      id,\n      tool: type,\n      data,\n      index,\n      needToFocus,\n      replace,\n    });\n\n    return new BlockAPI(insertedBlock);\n  };\n\n  /**\n   * Creates data of an empty block with a passed type.\n   *\n   * @param toolName - block tool name\n   */\n  public composeBlockData = async (toolName: string): Promise<BlockToolData> => {\n    const tool = this.Editor.Tools.blockTools.get(toolName);\n    const block = new Block({\n      tool,\n      api: this.Editor.API,\n      readOnly: true,\n      data: {},\n      tunesData: {},\n    });\n\n    return block.data;\n  };\n\n  /**\n   * Insert new Block\n   * After set caret to this Block\n   *\n   * @todo remove in 3.0.0\n   * @deprecated with insert() method\n   */\n  public insertNewBlock(): void {\n    _.log('Method blocks.insertNewBlock() is deprecated and it will be removed in the next major release. ' +\n      'Use blocks.insert() instead.', 'warn');\n    this.insert();\n  }\n\n  /**\n   * Updates block data by id\n   *\n   * @param id - id of the block to update\n   * @param data - (optional) the new data\n   * @param tunes - (optional) tune data\n   */\n  public update = async (id: string, data?: Partial<BlockToolData>, tunes?: {[name: string]: BlockTuneData}): Promise<BlockAPIInterface> => {\n    const { BlockManager } = this.Editor;\n    const block = BlockManager.getBlockById(id);\n\n    if (block === undefined) {\n      throw new Error(`Block with id \"${id}\" not found`);\n    }\n\n    const updatedBlock = await BlockManager.update(block, data, tunes);\n\n    // we cast to any because our BlockAPI has no \"new\" signature\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    return new (BlockAPI as any)(updatedBlock);\n  };\n\n  /**\n   * Converts block to another type. Both blocks should provide the conversionConfig.\n   *\n   * @param id - id of the existing block to convert. Should provide 'conversionConfig.export' method\n   * @param newType - new block type. Should provide 'conversionConfig.import' method\n   * @param dataOverrides - optional data overrides for the new block\n   * @throws Error if conversion is not possible\n   */\n  private convert = async (id: string, newType: string, dataOverrides?: BlockToolData): Promise<BlockAPIInterface> => {\n    const { BlockManager, Tools } = this.Editor;\n    const blockToConvert = BlockManager.getBlockById(id);\n\n    if (!blockToConvert) {\n      throw new Error(`Block with id \"${id}\" not found`);\n    }\n\n    const originalBlockTool = Tools.blockTools.get(blockToConvert.name);\n    const targetBlockTool = Tools.blockTools.get(newType);\n\n    if (!targetBlockTool) {\n      throw new Error(`Block Tool with type \"${newType}\" not found`);\n    }\n\n    const originalBlockConvertable = originalBlockTool?.conversionConfig?.export !== undefined;\n    const targetBlockConvertable = targetBlockTool.conversionConfig?.import !== undefined;\n\n    if (originalBlockConvertable && targetBlockConvertable) {\n      const newBlock = await BlockManager.convert(blockToConvert, newType, dataOverrides);\n\n      return new BlockAPI(newBlock);\n    } else {\n      const unsupportedBlockTypes = [\n        !originalBlockConvertable ? capitalize(blockToConvert.name) : false,\n        !targetBlockConvertable ? capitalize(newType) : false,\n      ].filter(Boolean).join(' and ');\n\n      throw new Error(`Conversion from \"${blockToConvert.name}\" to \"${newType}\" is not possible. ${unsupportedBlockTypes} tool(s) should provide a \"conversionConfig\"`);\n    }\n  };\n\n\n  /**\n   * Inserts several Blocks to a specified index\n   *\n   * @param blocks - blocks data to insert\n   * @param index - index to insert the blocks at\n   */\n  private insertMany = (\n    blocks: OutputBlockData[],\n    index: number = this.Editor.BlockManager.blocks.length - 1\n  ): BlockAPIInterface[] => {\n    this.validateIndex(index);\n\n    const blocksToInsert = blocks.map(({ id, type, data }) => {\n      return this.Editor.BlockManager.composeBlock({\n        id,\n        tool: type || (this.config.defaultBlock as string),\n        data,\n      });\n    });\n\n    this.Editor.BlockManager.insertMany(blocksToInsert, index);\n\n    // we cast to any because our BlockAPI has no \"new\" signature\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    return blocksToInsert.map((block) => new (BlockAPI as any)(block));\n  };\n\n  /**\n   * Validated block index and throws an error if it's invalid\n   *\n   * @param index - index to validate\n   */\n  private validateIndex(index: unknown): void {\n    if (typeof index !== 'number') {\n      throw new Error('Index should be a number');\n    }\n\n    if (index < 0) {\n      throw new Error(`Index should be greater than or equal to 0`);\n    }\n\n    if (index === null) {\n      throw new Error(`Index should be greater than or equal to 0`);\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/modules/api/caret.ts",
    "content": "import type { BlockAPI, Caret } from '../../../../types/api';\nimport Module from '../../__module';\nimport { resolveBlock } from '../../utils/api';\n\n/**\n * @class CaretAPI\n * provides with methods to work with caret\n */\nexport default class CaretAPI extends Module {\n  /**\n   * Available methods\n   *\n   * @returns {Caret}\n   */\n  public get methods(): Caret {\n    return {\n      setToFirstBlock: this.setToFirstBlock,\n      setToLastBlock: this.setToLastBlock,\n      setToPreviousBlock: this.setToPreviousBlock,\n      setToNextBlock: this.setToNextBlock,\n      setToBlock: this.setToBlock,\n      focus: this.focus,\n    };\n  }\n\n  /**\n   * Sets caret to the first Block\n   *\n   * @param {string} position - position where to set caret\n   * @param {number} offset - caret offset\n   * @returns {boolean}\n   */\n  private setToFirstBlock = (position: string = this.Editor.Caret.positions.DEFAULT, offset = 0): boolean => {\n    if (!this.Editor.BlockManager.firstBlock) {\n      return false;\n    }\n\n    this.Editor.Caret.setToBlock(this.Editor.BlockManager.firstBlock, position, offset);\n\n    return true;\n  };\n\n  /**\n   * Sets caret to the last Block\n   *\n   * @param {string} position - position where to set caret\n   * @param {number} offset - caret offset\n   * @returns {boolean}\n   */\n  private setToLastBlock = (position: string = this.Editor.Caret.positions.DEFAULT, offset = 0): boolean => {\n    if (!this.Editor.BlockManager.lastBlock) {\n      return false;\n    }\n\n    this.Editor.Caret.setToBlock(this.Editor.BlockManager.lastBlock, position, offset);\n\n    return true;\n  };\n\n  /**\n   * Sets caret to the previous Block\n   *\n   * @param {string} position - position where to set caret\n   * @param {number} offset - caret offset\n   * @returns {boolean}\n   */\n  private setToPreviousBlock = (\n    position: string = this.Editor.Caret.positions.DEFAULT,\n    offset = 0\n  ): boolean => {\n    if (!this.Editor.BlockManager.previousBlock) {\n      return false;\n    }\n\n    this.Editor.Caret.setToBlock(this.Editor.BlockManager.previousBlock, position, offset);\n\n    return true;\n  };\n\n  /**\n   * Sets caret to the next Block\n   *\n   * @param {string} position - position where to set caret\n   * @param {number} offset - caret offset\n   * @returns {boolean}\n   */\n  private setToNextBlock = (position: string = this.Editor.Caret.positions.DEFAULT, offset = 0): boolean => {\n    if (!this.Editor.BlockManager.nextBlock) {\n      return false;\n    }\n\n    this.Editor.Caret.setToBlock(this.Editor.BlockManager.nextBlock, position, offset);\n\n    return true;\n  };\n\n  /**\n   * Sets caret to the Block by passed index\n   *\n   * @param blockOrIdOrIndex - either BlockAPI or Block id or Block index\n   * @param position - position where to set caret\n   * @param offset - caret offset\n   * @returns {boolean}\n   */\n  private setToBlock = (\n    blockOrIdOrIndex: BlockAPI | BlockAPI['id'] | number,\n    position: string = this.Editor.Caret.positions.DEFAULT,\n    offset = 0\n  ): boolean => {\n    const block = resolveBlock(blockOrIdOrIndex, this.Editor);\n\n    if (block === undefined) {\n      return false;\n    }\n\n    this.Editor.Caret.setToBlock(block, position, offset);\n\n    return true;\n  };\n\n  /**\n   * Sets caret to the Editor\n   *\n   * @param {boolean} atEnd - if true, set Caret to the end of the Editor\n   * @returns {boolean}\n   */\n  private focus = (atEnd = false): boolean => {\n    if (atEnd) {\n      return this.setToLastBlock(this.Editor.Caret.positions.END);\n    }\n\n    return this.setToFirstBlock(this.Editor.Caret.positions.START);\n  };\n}\n"
  },
  {
    "path": "src/components/modules/api/events.ts",
    "content": "import Module from '../../__module';\nimport type { Events } from '../../../../types/api';\n\n/**\n * @class EventsAPI\n * provides with methods working with Toolbar\n */\nexport default class EventsAPI extends Module {\n  /**\n   * Available methods\n   *\n   * @returns {Events}\n   */\n  public get methods(): Events {\n    return {\n      emit: (eventName: string, data: object): void => this.emit(eventName, data),\n      off: (eventName: string, callback: () => void): void => this.off(eventName, callback),\n      on: (eventName: string, callback: () => void): void => this.on(eventName, callback),\n    };\n  }\n\n  /**\n   * Subscribe on Events\n   *\n   * @param {string} eventName - event name to subscribe\n   * @param {Function} callback - event handler\n   */\n  public on(eventName, callback): void {\n    this.eventsDispatcher.on(eventName, callback);\n  }\n\n  /**\n   * Emit event with data\n   *\n   * @param {string} eventName - event to emit\n   * @param {object} data - event's data\n   */\n  public emit(eventName, data): void {\n    this.eventsDispatcher.emit(eventName, data);\n  }\n\n  /**\n   * Unsubscribe from Event\n   *\n   * @param {string} eventName - event to unsubscribe\n   * @param {Function} callback - event handler\n   */\n  public off(eventName, callback): void {\n    this.eventsDispatcher.off(eventName, callback);\n  }\n}\n"
  },
  {
    "path": "src/components/modules/api/i18n.ts",
    "content": "import type { I18n } from '../../../../types/api';\nimport I18nInternal from '../../i18n';\nimport { logLabeled } from '../../utils';\nimport Module from '../../__module';\n\n/**\n * Provides methods for working with i18n\n */\nexport default class I18nAPI extends Module {\n  /**\n   * Return namespace section for tool or block tune\n   *\n   * @param toolName - tool name\n   * @param isTune - is tool a block tune\n   */\n  private static getNamespace(toolName, isTune): string {\n    if (isTune) {\n      return `blockTunes.${toolName}`;\n    }\n\n    return `tools.${toolName}`;\n  }\n\n  /**\n   * Return I18n API methods with global dictionary access\n   */\n  public get methods(): I18n {\n    return {\n      t: (): string | undefined => {\n        logLabeled('I18n.t() method can be accessed only from Tools', 'warn');\n\n        return undefined;\n      },\n    };\n  }\n\n  /**\n   * Return I18n API methods with tool namespaced dictionary\n   *\n   * @param toolName - tool name\n   * @param isTune - is tool a block tune\n   */\n  public getMethodsForTool(toolName: string, isTune: boolean): I18n {\n    return Object.assign(\n      this.methods,\n      {\n        t: (dictKey: string): string => {\n          return I18nInternal.t(I18nAPI.getNamespace(toolName, isTune), dictKey);\n        },\n      });\n  }\n}\n"
  },
  {
    "path": "src/components/modules/api/index.ts",
    "content": "/**\n * @module API\n * @copyright <CodeX> 2018\n *\n * Each block has an Editor API instance to use provided public methods\n * if you cant to read more about how API works, please see docs\n */\nimport Module from '../../__module';\nimport type { API as APIInterfaces } from '../../../../types';\n\n/**\n * @class API\n */\nexport default class API extends Module {\n  /**\n   * Editor.js Core API modules\n   */\n  public get methods(): APIInterfaces {\n    return {\n      blocks: this.Editor.BlocksAPI.methods,\n      caret: this.Editor.CaretAPI.methods,\n      tools: this.Editor.ToolsAPI.methods,\n      events: this.Editor.EventsAPI.methods,\n      listeners: this.Editor.ListenersAPI.methods,\n      notifier: this.Editor.NotifierAPI.methods,\n      sanitizer: this.Editor.SanitizerAPI.methods,\n      saver: this.Editor.SaverAPI.methods,\n      selection: this.Editor.SelectionAPI.methods,\n      styles: this.Editor.StylesAPI.classes,\n      toolbar: this.Editor.ToolbarAPI.methods,\n      inlineToolbar: this.Editor.InlineToolbarAPI.methods,\n      tooltip: this.Editor.TooltipAPI.methods,\n      i18n: this.Editor.I18nAPI.methods,\n      readOnly: this.Editor.ReadOnlyAPI.methods,\n      ui: this.Editor.UiAPI.methods,\n    };\n  }\n\n  /**\n   * Returns Editor.js Core API methods for passed tool\n   *\n   * @param toolName - tool name\n   * @param isTune - is tool a block tune\n   */\n  public getMethodsForTool(toolName: string, isTune: boolean): APIInterfaces {\n    return Object.assign(\n      this.methods,\n      {\n        i18n: this.Editor.I18nAPI.getMethodsForTool(toolName, isTune),\n      }\n    ) as APIInterfaces;\n  }\n}\n"
  },
  {
    "path": "src/components/modules/api/inlineToolbar.ts",
    "content": "import type { InlineToolbar } from '../../../../types/api/inline-toolbar';\nimport Module from '../../__module';\n\n/**\n * @class InlineToolbarAPI\n * Provides methods for working with the Inline Toolbar\n */\nexport default class InlineToolbarAPI extends Module {\n  /**\n   * Available methods\n   *\n   * @returns {InlineToolbar}\n   */\n  public get methods(): InlineToolbar {\n    return {\n      close: (): void => this.close(),\n      open: (): void => this.open(),\n    };\n  }\n\n  /**\n   * Open Inline Toolbar\n   */\n  public open(): void {\n    this.Editor.InlineToolbar.tryToShow();\n  }\n\n  /**\n   * Close Inline Toolbar\n   */\n  public close(): void {\n    this.Editor.InlineToolbar.close();\n  }\n}\n"
  },
  {
    "path": "src/components/modules/api/listeners.ts",
    "content": "import type { Listeners } from '../../../../types/api';\nimport Module from '../../__module';\n\n/**\n * @class ListenersAPI\n * Provides with methods working with DOM Listener\n */\nexport default class ListenersAPI extends Module {\n  /**\n   * Available methods\n   *\n   * @returns {Listeners}\n   */\n  public get methods(): Listeners {\n    return {\n      on: (element: HTMLElement, eventType, handler, useCapture): string => this.on(element, eventType, handler, useCapture),\n      off: (element, eventType, handler, useCapture): void => this.off(element, eventType, handler, useCapture),\n      offById: (id): void => this.offById(id),\n    };\n  }\n\n  /**\n   * Ads a DOM event listener. Return it's id.\n   *\n   * @param {HTMLElement} element - Element to set handler to\n   * @param {string} eventType - event type\n   * @param {() => void} handler - event handler\n   * @param {boolean} useCapture - capture event or not\n   */\n  public on(element: HTMLElement, eventType: string, handler: () => void, useCapture?: boolean): string {\n    return this.listeners.on(element, eventType, handler, useCapture);\n  }\n\n  /**\n   * Removes DOM listener from element\n   *\n   * @param {Element} element - Element to remove handler from\n   * @param eventType - event type\n   * @param handler - event handler\n   * @param {boolean} useCapture - capture event or not\n   */\n  public off(element: Element, eventType: string, handler: () => void, useCapture?: boolean): void {\n    this.listeners.off(element, eventType, handler, useCapture);\n  }\n\n  /**\n   * Removes DOM listener by the listener id\n   *\n   * @param id - id of the listener to remove\n   */\n  public offById(id: string): void {\n    this.listeners.offById(id);\n  }\n}\n"
  },
  {
    "path": "src/components/modules/api/notifier.ts",
    "content": "import type { Notifier as INotifier } from '../../../../types/api';\nimport Notifier from '../../utils/notifier';\nimport type { ConfirmNotifierOptions, NotifierOptions, PromptNotifierOptions } from 'codex-notifier';\nimport Module from '../../__module';\nimport type { ModuleConfig } from '../../../types-internal/module-config';\n\n/**\n *\n */\nexport default class NotifierAPI extends Module {\n  /**\n   * Notifier utility Instance\n   */\n  private notifier: Notifier;\n\n  /**\n   * @param moduleConfiguration - Module Configuration\n   * @param moduleConfiguration.config - Editor's config\n   * @param moduleConfiguration.eventsDispatcher - Editor's event dispatcher\n   */\n  constructor({ config, eventsDispatcher }: ModuleConfig) {\n    super({\n      config,\n      eventsDispatcher,\n    });\n\n    this.notifier = new Notifier();\n  }\n\n  /**\n   * Available methods\n   */\n  public get methods(): INotifier {\n    return {\n      show: (options: NotifierOptions | ConfirmNotifierOptions | PromptNotifierOptions): void => this.show(options),\n    };\n  }\n\n  /**\n   * Show notification\n   *\n   * @param {NotifierOptions} options - message option\n   */\n  public show(options: NotifierOptions | ConfirmNotifierOptions | PromptNotifierOptions): void {\n    return this.notifier.show(options);\n  }\n}\n"
  },
  {
    "path": "src/components/modules/api/readonly.ts",
    "content": "import type { ReadOnly } from '../../../../types/api';\nimport Module from '../../__module';\n\n/**\n * @class ReadOnlyAPI\n * @classdesc ReadOnly API\n */\nexport default class ReadOnlyAPI extends Module {\n  /**\n   * Available methods\n   */\n  public get methods(): ReadOnly {\n    const getIsEnabled = (): boolean => this.isEnabled;\n\n    // eslint-disable-next-line @typescript-eslint/no-this-alias\n    return {\n      toggle: (state): Promise<boolean> => this.toggle(state),\n      get isEnabled(): boolean {\n        return getIsEnabled();\n      },\n    };\n  }\n\n  /**\n   * Set or toggle read-only state\n   *\n   * @param {boolean|undefined} state - set or toggle state\n   * @returns {boolean} current value\n   */\n  public toggle(state?: boolean): Promise<boolean> {\n    return this.Editor.ReadOnly.toggle(state);\n  }\n\n  /**\n   * Returns current read-only state\n   */\n  public get isEnabled(): boolean {\n    return this.Editor.ReadOnly.isEnabled;\n  }\n}\n"
  },
  {
    "path": "src/components/modules/api/sanitizer.ts",
    "content": "import type { Sanitizer as ISanitizer } from '../../../../types/api';\nimport type { SanitizerConfig } from '../../../../types/configs';\nimport Module from '../../__module';\nimport { clean } from '../../utils/sanitizer';\n\n/**\n * @class SanitizerAPI\n * Provides Editor.js Sanitizer that allows developers to clean their HTML\n */\nexport default class SanitizerAPI extends Module {\n  /**\n   * Available methods\n   *\n   * @returns {SanitizerConfig}\n   */\n  public get methods(): ISanitizer {\n    return {\n      clean: (taintString, config): string => this.clean(taintString, config),\n    };\n  }\n\n  /**\n   * Perform sanitizing of a string\n   *\n   * @param {string} taintString - what to sanitize\n   * @param {SanitizerConfig} config - sanitizer config\n   * @returns {string}\n   */\n  public clean(taintString: string, config: SanitizerConfig): string {\n    return clean(taintString, config);\n  }\n}\n"
  },
  {
    "path": "src/components/modules/api/saver.ts",
    "content": "import type { Saver } from '../../../../types/api';\nimport type { OutputData } from '../../../../types';\nimport * as _ from '../../utils';\nimport Module from '../../__module';\n\n/**\n * @class SaverAPI\n * provides with methods to save data\n */\nexport default class SaverAPI extends Module {\n  /**\n   * Available methods\n   *\n   * @returns {Saver}\n   */\n  public get methods(): Saver {\n    return {\n      save: (): Promise<OutputData> => this.save(),\n    };\n  }\n\n  /**\n   * Return Editor's data\n   *\n   * @returns {OutputData}\n   */\n  public save(): Promise<OutputData> {\n    const errorText = 'Editor\\'s content can not be saved in read-only mode';\n\n    if (this.Editor.ReadOnly.isEnabled) {\n      _.logLabeled(errorText, 'warn');\n\n      return Promise.reject(new Error(errorText));\n    }\n\n    return this.Editor.Saver.save();\n  }\n}\n"
  },
  {
    "path": "src/components/modules/api/selection.ts",
    "content": "import SelectionUtils from '../../selection';\nimport type { Selection as SelectionAPIInterface } from '../../../../types/api';\nimport Module from '../../__module';\n\n/**\n * @class SelectionAPI\n * Provides with methods working with SelectionUtils\n */\nexport default class SelectionAPI extends Module {\n  /**\n   * Global SelectionUtils instance\n   */\n  private selectionUtils = new SelectionUtils();\n\n  /**\n   * Available methods\n   *\n   * @returns {SelectionAPIInterface}\n   */\n  public get methods(): SelectionAPIInterface {\n    return {\n      findParentTag: (tagName: string, className?: string): HTMLElement | null => this.findParentTag(tagName, className),\n      expandToTag: (node: HTMLElement): void => this.expandToTag(node),\n      save: () => this.selectionUtils.save(),\n      restore: () => this.selectionUtils.restore(),\n      setFakeBackground: () => this.selectionUtils.setFakeBackground(),\n      removeFakeBackground: () => this.selectionUtils.removeFakeBackground(),\n    };\n  }\n\n  /**\n   * Looks ahead from selection and find passed tag with class name\n   *\n   * @param {string} tagName - tag to find\n   * @param {string} className - tag's class name\n   * @returns {HTMLElement|null}\n   */\n  public findParentTag(tagName: string, className?: string): HTMLElement | null {\n    return this.selectionUtils.findParentTag(tagName, className);\n  }\n\n  /**\n   * Expand selection to passed tag\n   *\n   * @param {HTMLElement} node - tag that should contain selection\n   */\n  public expandToTag(node: HTMLElement): void {\n    this.selectionUtils.expandToTag(node);\n  }\n}\n"
  },
  {
    "path": "src/components/modules/api/styles.ts",
    "content": "import type { Styles } from '../../../../types/api';\nimport Module from '../../__module';\n\n/**\n *\n */\nexport default class StylesAPI extends Module {\n  /**\n   * Exported classes\n   */\n  public get classes(): Styles {\n    return {\n      /**\n       * Base Block styles\n       */\n      block: 'cdx-block',\n\n      /**\n       * Inline Tools styles\n       */\n      inlineToolButton: 'ce-inline-tool',\n      inlineToolButtonActive: 'ce-inline-tool--active',\n\n      /**\n       * UI elements\n       */\n      input: 'cdx-input',\n      loader: 'cdx-loader',\n      button: 'cdx-button',\n\n      /**\n       * Settings styles\n       */\n      settingsButton: 'cdx-settings-button',\n      settingsButtonActive: 'cdx-settings-button--active',\n    };\n  }\n}\n"
  },
  {
    "path": "src/components/modules/api/toolbar.ts",
    "content": "import type { Toolbar } from '../../../../types/api';\nimport Module from '../../__module';\nimport * as _ from './../../utils';\n/**\n * @class ToolbarAPI\n * Provides methods for working with the Toolbar\n */\nexport default class ToolbarAPI extends Module {\n  /**\n   * Available methods\n   *\n   * @returns {Toolbar}\n   */\n  public get methods(): Toolbar {\n    return {\n      close: (): void => this.close(),\n      open: (): void => this.open(),\n      toggleBlockSettings: (openingState?: boolean): void => this.toggleBlockSettings(openingState),\n      toggleToolbox: (openingState?: boolean): void => this.toggleToolbox(openingState),\n    };\n  }\n\n  /**\n   * Open toolbar\n   */\n  public open(): void {\n    this.Editor.Toolbar.moveAndOpen();\n  }\n\n  /**\n   * Close toolbar and all included elements\n   */\n  public close(): void {\n    this.Editor.Toolbar.close();\n  }\n\n  /**\n   * Toggles Block Setting of the current block\n   *\n   * @param {boolean} openingState —  opening state of Block Setting\n   */\n  public toggleBlockSettings(openingState?: boolean): void {\n    if (this.Editor.BlockManager.currentBlockIndex === -1) {\n      _.logLabeled('Could\\'t toggle the Toolbar because there is no block selected ', 'warn');\n\n      return;\n    }\n\n    /** Check that opening state is set or not */\n    const canOpenBlockSettings = openingState ?? !this.Editor.BlockSettings.opened;\n\n    if (canOpenBlockSettings) {\n      this.Editor.Toolbar.moveAndOpen();\n      this.Editor.BlockSettings.open();\n    } else {\n      this.Editor.BlockSettings.close();\n    }\n  }\n\n\n  /**\n   * Open toolbox\n   *\n   * @param {boolean} openingState - Opening state of toolbox\n   */\n  public toggleToolbox(openingState: boolean): void {\n    if (this.Editor.BlockManager.currentBlockIndex === -1) {\n      _.logLabeled('Could\\'t toggle the Toolbox because there is no block selected ', 'warn');\n\n      return;\n    }\n\n    const canOpenToolbox = openingState ?? !this.Editor.Toolbar.toolbox.opened;\n\n    if (canOpenToolbox) {\n      this.Editor.Toolbar.moveAndOpen();\n      this.Editor.Toolbar.toolbox.open();\n    } else {\n      this.Editor.Toolbar.toolbox.close();\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/modules/api/tools.ts",
    "content": "import type { Tools as ToolsAPIInterface } from '../../../../types/api';\nimport Module from '../../__module';\n\n/**\n * Provides methods for accessing installed Editor tools\n */\nexport default class ToolsAPI extends Module {\n  /**\n   * Available methods\n   */\n  public get methods(): ToolsAPIInterface {\n    return {\n      getBlockTools: () => Array.from(this.Editor.Tools.blockTools.values()),\n    };\n  }\n}\n"
  },
  {
    "path": "src/components/modules/api/tooltip.ts",
    "content": "import type { Tooltip as ITooltip } from '../../../../types/api';\nimport type { TooltipOptions, TooltipContent } from 'codex-tooltip/types';\nimport Module from '../../__module';\nimport type { ModuleConfig } from '../../../types-internal/module-config';\nimport * as tooltip from '../../utils/tooltip';\n/**\n * @class TooltipAPI\n * @classdesc Tooltip API\n */\nexport default class TooltipAPI extends Module {\n  /**\n   * @class\n   * @param moduleConfiguration - Module Configuration\n   * @param moduleConfiguration.config - Editor's config\n   * @param moduleConfiguration.eventsDispatcher - Editor's event dispatcher\n   */\n  constructor({ config, eventsDispatcher }: ModuleConfig) {\n    super({\n      config,\n      eventsDispatcher,\n    });\n  }\n\n  /**\n   * Available methods\n   */\n  public get methods(): ITooltip {\n    return {\n      show: (element: HTMLElement,\n        content: TooltipContent,\n        options?: TooltipOptions\n      ): void => this.show(element, content, options),\n      hide: (): void => this.hide(),\n      onHover: (element: HTMLElement,\n        content: TooltipContent,\n        options?: TooltipOptions\n      ): void => this.onHover(element, content, options),\n    };\n  }\n\n  /**\n   * Method show tooltip on element with passed HTML content\n   *\n   * @param {HTMLElement} element - element on which tooltip should be shown\n   * @param {TooltipContent} content - tooltip content\n   * @param {TooltipOptions} options - tooltip options\n   */\n  public show(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void {\n    tooltip.show(element, content, options);\n  }\n\n  /**\n   * Method hides tooltip on HTML page\n   */\n  public hide(): void {\n    tooltip.hide();\n  }\n\n  /**\n   * Decorator for showing Tooltip by mouseenter/mouseleave\n   *\n   * @param {HTMLElement} element - element on which tooltip should be shown\n   * @param {TooltipContent} content - tooltip content\n   * @param {TooltipOptions} options - tooltip options\n   */\n  public onHover(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void {\n    tooltip.onHover(element, content, options);\n  }\n}\n"
  },
  {
    "path": "src/components/modules/api/ui.ts",
    "content": "import Module from '../../__module';\nimport type { Ui, UiNodes } from '../../../../types/api';\n\n/**\n * API module allowing to access some Editor UI elements\n */\nexport default class UiAPI extends Module {\n  /**\n   * Available methods / getters\n   */\n  public get methods(): Ui {\n    return {\n      nodes: this.editorNodes,\n      /**\n       * There can be added some UI methods, like toggleThinMode() etc\n       */\n    };\n  }\n\n  /**\n   * Exported classes\n   */\n  private get editorNodes(): UiNodes {\n    return {\n      /**\n       * Top-level editor instance wrapper\n       */\n      wrapper: this.Editor.UI.nodes.wrapper,\n\n      /**\n       * Element that holds all the Blocks\n       */\n      redactor: this.Editor.UI.nodes.redactor,\n    };\n  }\n}\n"
  },
  {
    "path": "src/components/modules/blockEvents.ts",
    "content": "/**\n * Contains keyboard and mouse events bound on each Block by Block Manager\n */\nimport Module from '../__module';\nimport * as _ from '../utils';\nimport SelectionUtils from '../selection';\nimport Flipper from '../flipper';\nimport type Block from '../block';\nimport { areBlocksMergeable } from '../utils/blocks';\nimport * as caretUtils from '../utils/caret';\nimport { focus } from '@editorjs/caret';\n\n/**\n *\n */\nexport default class BlockEvents extends Module {\n  /**\n   * All keydowns on Block\n   *\n   * @param {KeyboardEvent} event - keydown\n   */\n  public keydown(event: KeyboardEvent): void {\n    /**\n     * Run common method for all keydown events\n     */\n    this.beforeKeydownProcessing(event);\n\n    /**\n     * Fire keydown processor by event.keyCode\n     */\n    switch (event.keyCode) {\n      case _.keyCodes.BACKSPACE:\n        this.backspace(event);\n        break;\n\n      case _.keyCodes.DELETE:\n        this.delete(event);\n        break;\n\n      case _.keyCodes.ENTER:\n        this.enter(event);\n        break;\n\n      case _.keyCodes.DOWN:\n      case _.keyCodes.RIGHT:\n        this.arrowRightAndDown(event);\n        break;\n\n      case _.keyCodes.UP:\n      case _.keyCodes.LEFT:\n        this.arrowLeftAndUp(event);\n        break;\n\n      case _.keyCodes.TAB:\n        this.tabPressed(event);\n        break;\n    }\n\n    /**\n     * We check for \"key\" here since on different keyboard layouts \"/\" can be typed as \"Shift + 7\" etc\n     *\n     * @todo probably using \"beforeInput\" event would be better here\n     */\n    if (event.key === '/' && !event.ctrlKey && !event.metaKey) {\n      this.slashPressed(event);\n    }\n\n    /**\n     * If user pressed \"Ctrl + /\" or \"Cmd + /\" — open Block Settings\n     * We check for \"code\" here since on different keyboard layouts there can be different keys in place of Slash.\n     */\n    if (event.code === 'Slash' && (event.ctrlKey || event.metaKey)) {\n      event.preventDefault();\n      this.commandSlashPressed();\n    }\n  }\n\n  /**\n   * Fires on keydown before event processing\n   *\n   * @param {KeyboardEvent} event - keydown\n   */\n  public beforeKeydownProcessing(event: KeyboardEvent): void {\n    /**\n     * Do not close Toolbox on Tabs or on Enter with opened Toolbox\n     */\n    if (!this.needToolbarClosing(event)) {\n      return;\n    }\n\n    /**\n     * When user type something:\n     *  - close Toolbar\n     *  - clear block highlighting\n     */\n    if (_.isPrintableKey(event.keyCode)) {\n      this.Editor.Toolbar.close();\n\n      /**\n       * Allow to use shortcuts with selected blocks\n       *\n       * @type {boolean}\n       */\n      const isShortcut = event.ctrlKey || event.metaKey || event.altKey || event.shiftKey;\n\n      if (!isShortcut) {\n        this.Editor.BlockSelection.clearSelection(event);\n      }\n    }\n  }\n\n  /**\n   * Key up on Block:\n   * - shows Inline Toolbar if something selected\n   * - shows conversion toolbar with 85% of block selection\n   *\n   * @param {KeyboardEvent} event - keyup event\n   */\n  public keyup(event: KeyboardEvent): void {\n    /**\n     * If shift key was pressed some special shortcut is used (eg. cross block selection via shift + arrows)\n     */\n    if (event.shiftKey) {\n      return;\n    }\n\n    /**\n     * Check if editor is empty on each keyup and add special css class to wrapper\n     */\n    this.Editor.UI.checkEmptiness();\n  }\n\n  /**\n   * Add drop target styles\n   *\n   * @param {DragEvent} event - drag over event\n   */\n  public dragOver(event: DragEvent): void {\n    const block = this.Editor.BlockManager.getBlockByChildNode(event.target as Node);\n\n    block.dropTarget = true;\n  }\n\n  /**\n   * Remove drop target style\n   *\n   * @param {DragEvent} event - drag leave event\n   */\n  public dragLeave(event: DragEvent): void {\n    const block = this.Editor.BlockManager.getBlockByChildNode(event.target as Node);\n\n    block.dropTarget = false;\n  }\n\n  /**\n   * Copying selected blocks\n   * Before putting to the clipboard we sanitize all blocks and then copy to the clipboard\n   *\n   * @param {ClipboardEvent} event - clipboard event\n   */\n  public handleCommandC(event: ClipboardEvent): void {\n    const { BlockSelection } = this.Editor;\n\n    if (!BlockSelection.anyBlockSelected) {\n      return;\n    }\n\n    // Copy Selected Blocks\n    BlockSelection.copySelectedBlocks(event);\n  }\n\n  /**\n   * Copy and Delete selected Blocks\n   *\n   * @param {ClipboardEvent} event - clipboard event\n   */\n  public handleCommandX(event: ClipboardEvent): void {\n    const { BlockSelection, BlockManager, Caret } = this.Editor;\n\n    if (!BlockSelection.anyBlockSelected) {\n      return;\n    }\n\n    BlockSelection.copySelectedBlocks(event).then(() => {\n      const selectionPositionIndex = BlockManager.removeSelectedBlocks();\n\n      /**\n       * Insert default block in place of removed ones\n       */\n      const insertedBlock = BlockManager.insertDefaultBlockAtIndex(selectionPositionIndex, true);\n\n      Caret.setToBlock(insertedBlock, Caret.positions.START);\n\n      /** Clear selection */\n      BlockSelection.clearSelection(event);\n    });\n  }\n\n  /**\n   * Tab pressed inside a Block.\n   *\n   * @param {KeyboardEvent} event - keydown\n   */\n  private tabPressed(event: KeyboardEvent): void {\n    const { InlineToolbar, Caret } = this.Editor;\n\n    const isFlipperActivated = InlineToolbar.opened;\n\n    if (isFlipperActivated) {\n      return;\n    }\n\n    const isNavigated = event.shiftKey ? Caret.navigatePrevious(true) : Caret.navigateNext(true);\n\n    /**\n     * If we have next Block/input to focus, then focus it. Otherwise, leave native Tab behaviour\n     */\n    if (isNavigated) {\n      event.preventDefault();\n    }\n  }\n\n  /**\n   * '/' + 'command' keydown inside a Block\n   */\n  private commandSlashPressed(): void {\n    if (this.Editor.BlockSelection.selectedBlocks.length > 1) {\n      return;\n    }\n\n    this.activateBlockSettings();\n  }\n\n  /**\n   * '/' keydown inside a Block\n   *\n   * @param event - keydown\n   */\n  private slashPressed(event: KeyboardEvent): void {\n    const wasEventTriggeredInsideEditor = this.Editor.UI.nodes.wrapper.contains(event.target as Node);\n\n    if (!wasEventTriggeredInsideEditor) {\n      return;\n    }\n\n    const currentBlock = this.Editor.BlockManager.currentBlock;\n    const canOpenToolbox = currentBlock.isEmpty;\n\n    /**\n     * @todo Handle case when slash pressed when several blocks are selected\n     */\n\n    /**\n     * Toolbox will be opened only if Block is empty\n     */\n    if (!canOpenToolbox) {\n      return;\n    }\n\n    /**\n     * The Toolbox will be opened with immediate focus on the Search input,\n     * and '/' will be added in the search input by default — we need to prevent it and add '/' manually\n     */\n    event.preventDefault();\n    this.Editor.Caret.insertContentAtCaretPosition('/');\n\n    this.activateToolbox();\n  }\n\n  /**\n   * ENTER pressed on block\n   *\n   * @param {KeyboardEvent} event - keydown\n   */\n  private enter(event: KeyboardEvent): void {\n    const { BlockManager, UI } = this.Editor;\n    const currentBlock = BlockManager.currentBlock;\n\n    if (currentBlock === undefined) {\n      return;\n    }\n\n    /**\n     * Don't handle Enter keydowns when Tool sets enableLineBreaks to true.\n     * Uses for Tools like <code> where line breaks should be handled by default behaviour.\n     */\n    if (currentBlock.tool.isLineBreaksEnabled) {\n      return;\n    }\n\n    /**\n     * Opened Toolbars uses Flipper with own Enter handling\n     * Allow split block when no one button in Flipper is focused\n     */\n    if (UI.someToolbarOpened && UI.someFlipperButtonFocused) {\n      return;\n    }\n\n    /**\n     * Allow to create line breaks by Shift+Enter\n     *\n     * Note. On iOS devices, Safari automatically treats enter after a period+space (\". |\") as Shift+Enter\n     * (it used for capitalizing of the first letter of the next sentence)\n     * We don't need to lead soft line break in this case — new block should be created\n     */\n    if (event.shiftKey && !_.isIosDevice) {\n      return;\n    }\n\n    let blockToFocus = currentBlock;\n\n    /**\n     * If enter has been pressed at the start of the text, just insert paragraph Block above\n     */\n    if (currentBlock.currentInput !== undefined && caretUtils.isCaretAtStartOfInput(currentBlock.currentInput) && !currentBlock.hasMedia) {\n      this.Editor.BlockManager.insertDefaultBlockAtIndex(this.Editor.BlockManager.currentBlockIndex);\n\n    /**\n     * If caret is at very end of the block, just append the new block without splitting\n     * to prevent unnecessary dom mutation observing\n     */\n    } else if (currentBlock.currentInput && caretUtils.isCaretAtEndOfInput(currentBlock.currentInput)) {\n      blockToFocus = this.Editor.BlockManager.insertDefaultBlockAtIndex(this.Editor.BlockManager.currentBlockIndex + 1);\n    } else {\n      /**\n       * Split the Current Block into two blocks\n       * Renew local current node after split\n       */\n      blockToFocus = this.Editor.BlockManager.split();\n    }\n\n    this.Editor.Caret.setToBlock(blockToFocus);\n\n    /**\n     * Show Toolbar\n     */\n    this.Editor.Toolbar.moveAndOpen(blockToFocus);\n\n    event.preventDefault();\n  }\n\n  /**\n   * Handle backspace keydown on Block\n   *\n   * @param {KeyboardEvent} event - keydown\n   */\n  private backspace(event: KeyboardEvent): void {\n    const { BlockManager, Caret } = this.Editor;\n    const { currentBlock, previousBlock } = BlockManager;\n\n    if (currentBlock === undefined) {\n      return;\n    }\n\n    /**\n     * If some fragment is selected, leave native behaviour\n     */\n    if (!SelectionUtils.isCollapsed) {\n      return;\n    }\n\n    /**\n     * If caret is not at the start, leave native behaviour\n     */\n    if (!currentBlock.currentInput || !caretUtils.isCaretAtStartOfInput(currentBlock.currentInput)) {\n      return;\n    }\n    /**\n     * All the cases below have custom behaviour, so we don't need a native one\n     */\n    event.preventDefault();\n    this.Editor.Toolbar.close();\n\n    const isFirstInputFocused = currentBlock.currentInput === currentBlock.firstInput;\n\n    /**\n     * For example, caret at the start of the Quote second input (caption) — just navigate previous input\n     */\n    if (!isFirstInputFocused) {\n      Caret.navigatePrevious();\n\n      return;\n    }\n\n    /**\n     * Backspace at the start of the first Block should do nothing\n     */\n    if (previousBlock === null) {\n      return;\n    }\n\n    /**\n     * If prev Block is empty, it should be removed just like a character\n     */\n    if (previousBlock.isEmpty) {\n      BlockManager.removeBlock(previousBlock);\n\n      return;\n    }\n\n    /**\n     * If current Block is empty, just remove it and set cursor to the previous Block (like we're removing line break char)\n     */\n    if (currentBlock.isEmpty) {\n      BlockManager.removeBlock(currentBlock);\n\n      const newCurrentBlock = BlockManager.currentBlock;\n\n      Caret.setToBlock(newCurrentBlock, Caret.positions.END);\n\n      return;\n    }\n\n    const bothBlocksMergeable = areBlocksMergeable(previousBlock, currentBlock);\n\n    /**\n     * If Blocks could be merged, do it\n     * Otherwise, just navigate previous block\n     */\n    if (bothBlocksMergeable) {\n      this.mergeBlocks(previousBlock, currentBlock);\n    } else {\n      Caret.setToBlock(previousBlock, Caret.positions.END);\n    }\n  }\n\n  /**\n   * Handles delete keydown on Block\n   * Removes char after the caret.\n   * If caret is at the end of the block, merge next block with current\n   *\n   * @param {KeyboardEvent} event - keydown\n   */\n  private delete(event: KeyboardEvent): void {\n    const { BlockManager, Caret } = this.Editor;\n    const { currentBlock, nextBlock } = BlockManager;\n\n    /**\n     * If some fragment is selected, leave native behaviour\n     */\n    if (!SelectionUtils.isCollapsed) {\n      return;\n    }\n\n    /**\n     * If caret is not at the end, leave native behaviour\n     */\n    if (!caretUtils.isCaretAtEndOfInput(currentBlock.currentInput)) {\n      return;\n    }\n\n    /**\n     * All the cases below have custom behaviour, so we don't need a native one\n     */\n    event.preventDefault();\n    this.Editor.Toolbar.close();\n\n    const isLastInputFocused = currentBlock.currentInput === currentBlock.lastInput;\n\n    /**\n     * For example, caret at the end of the Quote first input (quote text) — just navigate next input (caption)\n     */\n    if (!isLastInputFocused) {\n      Caret.navigateNext();\n\n      return;\n    }\n\n    /**\n     * Delete at the end of the last Block should do nothing\n     */\n    if (nextBlock === null) {\n      return;\n    }\n\n    /**\n     * If next Block is empty, it should be removed just like a character\n     */\n    if (nextBlock.isEmpty) {\n      BlockManager.removeBlock(nextBlock);\n\n      return;\n    }\n\n    /**\n     * If current Block is empty, just remove it and set cursor to the next Block (like we're removing line break char)\n     */\n    if (currentBlock.isEmpty) {\n      BlockManager.removeBlock(currentBlock);\n\n      Caret.setToBlock(nextBlock, Caret.positions.START);\n\n      return;\n    }\n\n    const bothBlocksMergeable = areBlocksMergeable(currentBlock, nextBlock);\n\n    /**\n     * If Blocks could be merged, do it\n     * Otherwise, just navigate to the next block\n     */\n    if (bothBlocksMergeable) {\n      this.mergeBlocks(currentBlock, nextBlock);\n    } else {\n      Caret.setToBlock(nextBlock, Caret.positions.START);\n    }\n  }\n\n  /**\n   * Merge passed Blocks\n   *\n   * @param targetBlock - to which Block we want to merge\n   * @param blockToMerge - what Block we want to merge\n   */\n  private mergeBlocks(targetBlock: Block, blockToMerge: Block): void {\n    const { BlockManager, Toolbar } = this.Editor;\n\n    if (targetBlock.lastInput === undefined) {\n      return;\n    }\n\n    focus(targetBlock.lastInput, false);\n\n    BlockManager\n      .mergeBlocks(targetBlock, blockToMerge)\n      .then(() => {\n        Toolbar.close();\n      });\n  }\n\n  /**\n   * Handle right and down keyboard keys\n   *\n   * @param {KeyboardEvent} event - keyboard event\n   */\n  private arrowRightAndDown(event: KeyboardEvent): void {\n    const isFlipperCombination = Flipper.usedKeys.includes(event.keyCode) &&\n      (!event.shiftKey || event.keyCode === _.keyCodes.TAB);\n\n    /**\n     * Arrows might be handled on toolbars by flipper\n     * Check for Flipper.usedKeys to allow navigate by DOWN and disallow by RIGHT\n     */\n    if (this.Editor.UI.someToolbarOpened && isFlipperCombination) {\n      return;\n    }\n\n    /**\n     * Close Toolbar when user moves cursor\n     */\n    this.Editor.Toolbar.close();\n\n    const { currentBlock } = this.Editor.BlockManager;\n    const caretAtEnd = currentBlock?.currentInput !== undefined ? caretUtils.isCaretAtEndOfInput(currentBlock.currentInput) : undefined;\n    const shouldEnableCBS = caretAtEnd || this.Editor.BlockSelection.anyBlockSelected;\n\n    if (event.shiftKey && event.keyCode === _.keyCodes.DOWN && shouldEnableCBS) {\n      this.Editor.CrossBlockSelection.toggleBlockSelectedState();\n\n      return;\n    }\n\n    const navigateNext = event.keyCode === _.keyCodes.DOWN || (event.keyCode === _.keyCodes.RIGHT && !this.isRtl);\n    const isNavigated = navigateNext ? this.Editor.Caret.navigateNext() : this.Editor.Caret.navigatePrevious();\n\n    if (isNavigated) {\n      /**\n       * Default behaviour moves cursor by 1 character, we need to prevent it\n       */\n      event.preventDefault();\n\n      return;\n    }\n\n    /**\n     * After caret is set, update Block input index\n     */\n    _.delay(() => {\n      /** Check currentBlock for case when user moves selection out of Editor */\n      if (this.Editor.BlockManager.currentBlock) {\n        this.Editor.BlockManager.currentBlock.updateCurrentInput();\n      }\n    // eslint-disable-next-line @typescript-eslint/no-magic-numbers\n    }, 20)();\n\n    /**\n     * Clear blocks selection by arrows\n     */\n    this.Editor.BlockSelection.clearSelection(event);\n  }\n\n  /**\n   * Handle left and up keyboard keys\n   *\n   * @param {KeyboardEvent} event - keyboard event\n   */\n  private arrowLeftAndUp(event: KeyboardEvent): void {\n    /**\n     * Arrows might be handled on toolbars by flipper\n     * Check for Flipper.usedKeys to allow navigate by UP and disallow by LEFT\n     */\n    if (this.Editor.UI.someToolbarOpened) {\n      if (Flipper.usedKeys.includes(event.keyCode) && (!event.shiftKey || event.keyCode === _.keyCodes.TAB)) {\n        return;\n      }\n\n      this.Editor.UI.closeAllToolbars();\n    }\n\n    /**\n     * Close Toolbar when user moves cursor\n     */\n    this.Editor.Toolbar.close();\n\n    const { currentBlock } = this.Editor.BlockManager;\n    const caretAtStart = currentBlock?.currentInput !== undefined ? caretUtils.isCaretAtStartOfInput(currentBlock.currentInput) : undefined;\n    const shouldEnableCBS = caretAtStart || this.Editor.BlockSelection.anyBlockSelected;\n\n    if (event.shiftKey && event.keyCode === _.keyCodes.UP && shouldEnableCBS) {\n      this.Editor.CrossBlockSelection.toggleBlockSelectedState(false);\n\n      return;\n    }\n\n    const navigatePrevious = event.keyCode === _.keyCodes.UP || (event.keyCode === _.keyCodes.LEFT && !this.isRtl);\n    const isNavigated = navigatePrevious ? this.Editor.Caret.navigatePrevious() : this.Editor.Caret.navigateNext();\n\n    if (isNavigated) {\n      /**\n       * Default behaviour moves cursor by 1 character, we need to prevent it\n       */\n      event.preventDefault();\n\n      return;\n    }\n\n    /**\n     * After caret is set, update Block input index\n     */\n    _.delay(() => {\n      /** Check currentBlock for case when user ends selection out of Editor and then press arrow-key */\n      if (this.Editor.BlockManager.currentBlock) {\n        this.Editor.BlockManager.currentBlock.updateCurrentInput();\n      }\n    // eslint-disable-next-line @typescript-eslint/no-magic-numbers\n    }, 20)();\n\n    /**\n     * Clear blocks selection by arrows\n     */\n    this.Editor.BlockSelection.clearSelection(event);\n  }\n\n  /**\n   * Cases when we need to close Toolbar\n   *\n   * @param {KeyboardEvent} event - keyboard event\n   */\n  private needToolbarClosing(event: KeyboardEvent): boolean {\n    const toolboxItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.Toolbar.toolbox.opened),\n        blockSettingsItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.BlockSettings.opened),\n        inlineToolbarItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.InlineToolbar.opened),\n        flippingToolbarItems = event.keyCode === _.keyCodes.TAB;\n\n    /**\n     * Do not close Toolbar in cases:\n     * 1. ShiftKey pressed (or combination with shiftKey)\n     * 2. When Toolbar is opened and Tab leafs its Tools\n     * 3. When Toolbar's component is opened and some its item selected\n     */\n    return !(event.shiftKey ||\n      flippingToolbarItems ||\n      toolboxItemSelected ||\n      blockSettingsItemSelected ||\n      inlineToolbarItemSelected\n    );\n  }\n\n  /**\n   * If Toolbox is not open, then just open it and show plus button\n   */\n  private activateToolbox(): void {\n    if (!this.Editor.Toolbar.opened) {\n      this.Editor.Toolbar.moveAndOpen();\n    } // else Flipper will leaf through it\n\n    this.Editor.Toolbar.toolbox.open();\n  }\n\n  /**\n   * Open Toolbar and show BlockSettings before flipping Tools\n   */\n  private activateBlockSettings(): void {\n    if (!this.Editor.Toolbar.opened) {\n      this.Editor.Toolbar.moveAndOpen();\n    }\n\n    /**\n     * If BlockSettings is not open, then open BlockSettings\n     * Next Tab press will leaf Settings Buttons\n     */\n    if (!this.Editor.BlockSettings.opened) {\n      /**\n       * @todo Debug the case when we set caret to some block, hovering another block\n       *       — wrong settings will be opened.\n       *       To fix it, we should refactor the Block Settings module — make it a standalone class, like the Toolbox\n       */\n      this.Editor.BlockSettings.open();\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/modules/blockManager.ts",
    "content": "/**\n * @class BlockManager\n * @classdesc Manage editor`s blocks storage and appearance\n * @module BlockManager\n * @version 2.0.0\n */\nimport Block, { BlockToolAPI } from '../block';\nimport Module from '../__module';\nimport $ from '../dom';\nimport * as _ from '../utils';\nimport Blocks from '../blocks';\nimport type { BlockToolData, PasteEvent } from '../../../types';\nimport type { BlockTuneData } from '../../../types/block-tunes/block-tune-data';\nimport BlockAPI from '../block/api';\nimport type { BlockMutationEventMap, BlockMutationType } from '../../../types/events/block';\nimport { BlockRemovedMutationType } from '../../../types/events/block/BlockRemoved';\nimport { BlockAddedMutationType } from '../../../types/events/block/BlockAdded';\nimport { BlockMovedMutationType } from '../../../types/events/block/BlockMoved';\nimport { BlockChangedMutationType } from '../../../types/events/block/BlockChanged';\nimport { BlockChanged } from '../events';\nimport { clean, sanitizeBlocks } from '../utils/sanitizer';\nimport { convertStringToBlockData, isBlockConvertable } from '../utils/blocks';\nimport PromiseQueue from '../utils/promise-queue';\n\n/**\n * @typedef {BlockManager} BlockManager\n * @property {number} currentBlockIndex - Index of current working block\n * @property {Proxy} _blocks - Proxy for Blocks instance {@link Blocks}\n */\nexport default class BlockManager extends Module {\n  /**\n   * Returns current Block index\n   *\n   * @returns {number}\n   */\n  public get currentBlockIndex(): number {\n    return this._currentBlockIndex;\n  }\n\n  /**\n   * Set current Block index and fire Block lifecycle callbacks\n   *\n   * @param {number} newIndex - index of Block to set as current\n   */\n  public set currentBlockIndex(newIndex: number) {\n    this._currentBlockIndex = newIndex;\n  }\n\n  /**\n   * returns first Block\n   *\n   * @returns {Block}\n   */\n  public get firstBlock(): Block {\n    return this._blocks[0];\n  }\n\n  /**\n   * returns last Block\n   *\n   * @returns {Block}\n   */\n  public get lastBlock(): Block {\n    return this._blocks[this._blocks.length - 1];\n  }\n\n  /**\n   * Get current Block instance\n   *\n   * @returns {Block}\n   */\n  public get currentBlock(): Block | undefined {\n    return this._blocks[this.currentBlockIndex];\n  }\n\n  /**\n   * Set passed Block as a current\n   *\n   * @param block - block to set as a current\n   */\n  public set currentBlock(block: Block) {\n    this.currentBlockIndex = this.getBlockIndex(block);\n  }\n\n  /**\n   * Returns next Block instance\n   *\n   * @returns {Block|null}\n   */\n  public get nextBlock(): Block | null {\n    const isLastBlock = this.currentBlockIndex === (this._blocks.length - 1);\n\n    if (isLastBlock) {\n      return null;\n    }\n\n    return this._blocks[this.currentBlockIndex + 1];\n  }\n\n  /**\n   * Return first Block with inputs after current Block\n   *\n   * @returns {Block | undefined}\n   */\n  public get nextContentfulBlock(): Block {\n    const nextBlocks = this.blocks.slice(this.currentBlockIndex + 1);\n\n    return nextBlocks.find((block) => !!block.inputs.length);\n  }\n\n  /**\n   * Return first Block with inputs before current Block\n   *\n   * @returns {Block | undefined}\n   */\n  public get previousContentfulBlock(): Block {\n    const previousBlocks = this.blocks.slice(0, this.currentBlockIndex).reverse();\n\n    return previousBlocks.find((block) => !!block.inputs.length);\n  }\n\n  /**\n   * Returns previous Block instance\n   *\n   * @returns {Block|null}\n   */\n  public get previousBlock(): Block | null {\n    const isFirstBlock = this.currentBlockIndex === 0;\n\n    if (isFirstBlock) {\n      return null;\n    }\n\n    return this._blocks[this.currentBlockIndex - 1];\n  }\n\n  /**\n   * Get array of Block instances\n   *\n   * @returns {Block[]} {@link Blocks#array}\n   */\n  public get blocks(): Block[] {\n    return this._blocks.array;\n  }\n\n  /**\n   * Check if each Block is empty\n   *\n   * @returns {boolean}\n   */\n  public get isEditorEmpty(): boolean {\n    return this.blocks.every((block) => block.isEmpty);\n  }\n\n  /**\n   * Index of current working block\n   *\n   * @type {number}\n   */\n  private _currentBlockIndex = -1;\n\n  /**\n   * Proxy for Blocks instance {@link Blocks}\n   *\n   * @type {Proxy}\n   * @private\n   */\n  private _blocks: Blocks = null;\n\n  /**\n   * Should be called after Editor.UI preparation\n   * Define this._blocks property\n   */\n  public prepare(): void {\n    const blocks = new Blocks(this.Editor.UI.nodes.redactor);\n\n    /**\n     * We need to use Proxy to overload set/get [] operator.\n     * So we can use array-like syntax to access blocks\n     *\n     * @example\n     * this._blocks[0] = new Block(...);\n     *\n     * block = this._blocks[0];\n     * @todo proxy the enumerate method\n     * @type {Proxy}\n     * @private\n     */\n    this._blocks = new Proxy(blocks, {\n      set: Blocks.set,\n      get: Blocks.get,\n    });\n\n    /** Copy event */\n    this.listeners.on(\n      document,\n      'copy',\n      (e: ClipboardEvent) => this.Editor.BlockEvents.handleCommandC(e)\n    );\n  }\n\n  /**\n   * Toggle read-only state\n   *\n   * If readOnly is true:\n   *  - Unbind event handlers from created Blocks\n   *\n   * if readOnly is false:\n   *  - Bind event handlers to all existing Blocks\n   *\n   * @param {boolean} readOnlyEnabled - \"read only\" state\n   */\n  public toggleReadOnly(readOnlyEnabled: boolean): void {\n    if (!readOnlyEnabled) {\n      this.enableModuleBindings();\n    } else {\n      this.disableModuleBindings();\n    }\n  }\n\n  /**\n   * Creates Block instance by tool name\n   *\n   * @param {object} options - block creation options\n   * @param {string} options.tool - tools passed in editor config {@link EditorConfig#tools}\n   * @param {string} [options.id] - unique id for this block\n   * @param {BlockToolData} [options.data] - constructor params\n   * @returns {Block}\n   */\n  public composeBlock({\n    tool: name,\n    data = {},\n    id = undefined,\n    tunes: tunesData = {},\n  }: {tool: string; id?: string; data?: BlockToolData; tunes?: {[name: string]: BlockTuneData}}): Block {\n    const readOnly = this.Editor.ReadOnly.isEnabled;\n    const tool = this.Editor.Tools.blockTools.get(name);\n    const block = new Block({\n      id,\n      data,\n      tool,\n      api: this.Editor.API,\n      readOnly,\n      tunesData,\n    }, this.eventsDispatcher);\n\n    if (!readOnly) {\n      window.requestIdleCallback(() => {\n        this.bindBlockEvents(block);\n      }, { timeout: 2000 });\n    }\n\n    return block;\n  }\n\n  /**\n   * Insert new block into _blocks\n   *\n   * @param {object} options - insert options\n   * @param {string} [options.id] - block's unique id\n   * @param {string} [options.tool] - plugin name, by default method inserts the default block type\n   * @param {object} [options.data] - plugin data\n   * @param {number} [options.index] - index where to insert new Block\n   * @param {boolean} [options.needToFocus] - flag shows if needed to update current Block index\n   * @param {boolean} [options.replace] - flag shows if block by passed index should be replaced with inserted one\n   * @returns {Block}\n   */\n  public insert({\n    id = undefined,\n    tool = this.config.defaultBlock,\n    data = {},\n    index,\n    needToFocus = true,\n    replace = false,\n    tunes = {},\n  }: {\n    id?: string;\n    tool?: string;\n    data?: BlockToolData;\n    index?: number;\n    needToFocus?: boolean;\n    replace?: boolean;\n    tunes?: {[name: string]: BlockTuneData};\n  } = {}): Block {\n    let newIndex = index;\n\n    if (newIndex === undefined) {\n      newIndex = this.currentBlockIndex + (replace ? 0 : 1);\n    }\n\n    const block = this.composeBlock({\n      id,\n      tool,\n      data,\n      tunes,\n    });\n\n    /**\n     * In case of block replacing (Converting OR from Toolbox or Shortcut on empty block OR on-paste to empty block)\n     * we need to dispatch the 'block-removing' event for the replacing block\n     */\n    if (replace) {\n      this.blockDidMutated(BlockRemovedMutationType, this.getBlockByIndex(newIndex), {\n        index: newIndex,\n      });\n    }\n\n    this._blocks.insert(newIndex, block, replace);\n\n    /**\n     * Force call of didMutated event on Block insertion\n     */\n    this.blockDidMutated(BlockAddedMutationType, block, {\n      index: newIndex,\n    });\n\n    if (needToFocus) {\n      this.currentBlockIndex = newIndex;\n    } else if (newIndex <= this.currentBlockIndex) {\n      this.currentBlockIndex++;\n    }\n\n    return block;\n  }\n\n  /**\n   * Inserts several blocks at once\n   *\n   * @param blocks - blocks to insert\n   * @param index - index where to insert\n   */\n  public insertMany(blocks: Block[], index = 0): void {\n    this._blocks.insertMany(blocks, index);\n  }\n\n  /**\n   * Update Block data.\n   *\n   * Currently we don't have an 'update' method in the Tools API, so we just create a new block with the same id and type\n   * Should not trigger 'block-removed' or 'block-added' events.\n   *\n   * If neither data nor tunes is provided, return the provided block instead.\n   *\n   * @param block - block to update\n   * @param data - (optional) new data\n   * @param tunes - (optional) tune data\n   */\n  public async update(block: Block, data?: Partial<BlockToolData>, tunes?: {[name: string]: BlockTuneData}): Promise<Block> {\n    if (!data && !tunes) {\n      return block;\n    }\n\n    const existingData = await block.data;\n\n    const newBlock = this.composeBlock({\n      id: block.id,\n      tool: block.name,\n      data: Object.assign({}, existingData, data ?? {}),\n      tunes: tunes ?? block.tunes,\n    });\n\n    const blockIndex = this.getBlockIndex(block);\n\n    this._blocks.replace(blockIndex, newBlock);\n\n    this.blockDidMutated(BlockChangedMutationType, newBlock, {\n      index: blockIndex,\n    });\n\n    return newBlock;\n  }\n\n  /**\n   * Replace passed Block with the new one with specified Tool and data\n   *\n   * @param block - block to replace\n   * @param newTool - new Tool name\n   * @param data - new Tool data\n   */\n  public replace(block: Block, newTool: string, data: BlockToolData): Block {\n    const blockIndex = this.getBlockIndex(block);\n\n    return this.insert({\n      tool: newTool,\n      data,\n      index: blockIndex,\n      replace: true,\n    });\n  }\n\n  /**\n   * Insert pasted content. Call onPaste callback after insert.\n   *\n   * @param {string} toolName - name of Tool to insert\n   * @param {PasteEvent} pasteEvent - pasted data\n   * @param {boolean} replace - should replace current block\n   */\n  public paste(\n    toolName: string,\n    pasteEvent: PasteEvent,\n    replace = false\n  ): Block {\n    const block = this.insert({\n      tool: toolName,\n      replace,\n    });\n\n    try {\n      /**\n       * We need to call onPaste after Block will be ready\n       * because onPaste could change tool's root element, and we need to do that after block.watchBlockMutations() bound\n       * to detect tool root element change\n       *\n       * @todo make this.insert() awaitable and remove requestIdleCallback\n       */\n      window.requestIdleCallback(() => {\n        block.call(BlockToolAPI.ON_PASTE, pasteEvent);\n      });\n    } catch (e) {\n      _.log(`${toolName}: onPaste callback call is failed`, 'error', e);\n    }\n\n    return block;\n  }\n\n  /**\n   * Insert new default block at passed index\n   *\n   * @param {number} index - index where Block should be inserted\n   * @param {boolean} needToFocus - if true, updates current Block index\n   *\n   * TODO: Remove method and use insert() with index instead (?)\n   * @returns {Block} inserted Block\n   */\n  public insertDefaultBlockAtIndex(index: number, needToFocus = false): Block {\n    const block = this.composeBlock({ tool: this.config.defaultBlock });\n\n    this._blocks[index] = block;\n\n    /**\n     * Force call of didMutated event on Block insertion\n     */\n    this.blockDidMutated(BlockAddedMutationType, block, {\n      index,\n    });\n\n    if (needToFocus) {\n      this.currentBlockIndex = index;\n    } else if (index <= this.currentBlockIndex) {\n      this.currentBlockIndex++;\n    }\n\n    return block;\n  }\n\n  /**\n   * Always inserts at the end\n   *\n   * @returns {Block}\n   */\n  public insertAtEnd(): Block {\n    /**\n     * Define new value for current block index\n     */\n    this.currentBlockIndex = this.blocks.length - 1;\n\n    /**\n     * Insert the default typed block\n     */\n    return this.insert();\n  }\n\n  /**\n   * Merge two blocks\n   *\n   * @param {Block} targetBlock - previous block will be append to this block\n   * @param {Block} blockToMerge - block that will be merged with target block\n   * @returns {Promise} - the sequence that can be continued\n   */\n  public async mergeBlocks(targetBlock: Block, blockToMerge: Block): Promise<void> {\n    let blockToMergeData: BlockToolData | undefined;\n\n    /**\n     * We can merge:\n     * 1) Blocks with the same Tool if tool provides merge method\n     */\n    if (targetBlock.name === blockToMerge.name && targetBlock.mergeable) {\n      const blockToMergeDataRaw = await blockToMerge.data;\n\n      if (_.isEmpty(blockToMergeDataRaw)) {\n        console.error('Could not merge Block. Failed to extract original Block data.');\n\n        return;\n      }\n\n      const [ cleanData ] = sanitizeBlocks([ blockToMergeDataRaw ], targetBlock.tool.sanitizeConfig);\n\n      blockToMergeData = cleanData;\n\n    /**\n     * 2) Blocks with different Tools if they provides conversionConfig\n     */\n    } else if (targetBlock.mergeable && isBlockConvertable(blockToMerge, 'export') && isBlockConvertable(targetBlock, 'import')) {\n      const blockToMergeDataStringified = await blockToMerge.exportDataAsString();\n      const cleanData = clean(blockToMergeDataStringified, targetBlock.tool.sanitizeConfig);\n\n      blockToMergeData = convertStringToBlockData(cleanData, targetBlock.tool.conversionConfig);\n    }\n\n    if (blockToMergeData === undefined) {\n      return;\n    }\n\n    await targetBlock.mergeWith(blockToMergeData);\n    this.removeBlock(blockToMerge);\n    this.currentBlockIndex = this._blocks.indexOf(targetBlock);\n  }\n\n  /**\n   * Remove passed Block\n   *\n   * @param block - Block to remove\n   * @param addLastBlock - if true, adds new default block at the end. @todo remove this logic and use event-bus instead\n   */\n  public removeBlock(block: Block, addLastBlock = true): Promise<void> {\n    return new Promise((resolve) => {\n      const index = this._blocks.indexOf(block);\n\n      /**\n       * If index is not passed and there is no block selected, show a warning\n       */\n      if (!this.validateIndex(index)) {\n        throw new Error('Can\\'t find a Block to remove');\n      }\n\n      this._blocks.remove(index);\n      block.destroy();\n\n      /**\n       * Force call of didMutated event on Block removal\n       */\n      this.blockDidMutated(BlockRemovedMutationType, block, {\n        index,\n      });\n\n      if (this.currentBlockIndex >= index) {\n        this.currentBlockIndex--;\n      }\n\n      /**\n       * If first Block was removed, insert new Initial Block and set focus on it`s first input\n       */\n      if (!this.blocks.length) {\n        this.unsetCurrentBlock();\n\n        if (addLastBlock) {\n          this.insert();\n        }\n      } else if (index === 0) {\n        this.currentBlockIndex = 0;\n      }\n\n      resolve();\n    });\n  }\n\n  /**\n   * Remove only selected Blocks\n   * and returns first Block index where started removing...\n   *\n   * @returns {number|undefined}\n   */\n  public removeSelectedBlocks(): number | undefined {\n    let firstSelectedBlockIndex;\n\n    /**\n     * Remove selected Blocks from the end\n     */\n    for (let index = this.blocks.length - 1; index >= 0; index--) {\n      if (!this.blocks[index].selected) {\n        continue;\n      }\n\n      this.removeBlock(this.blocks[index]);\n      firstSelectedBlockIndex = index;\n    }\n\n    return firstSelectedBlockIndex;\n  }\n\n  /**\n   * Attention!\n   * After removing insert the new default typed Block and focus on it\n   * Removes all blocks\n   */\n  public removeAllBlocks(): void {\n    for (let index = this.blocks.length - 1; index >= 0; index--) {\n      this._blocks.remove(index);\n    }\n\n    this.unsetCurrentBlock();\n    this.insert();\n    this.currentBlock.firstInput.focus();\n  }\n\n  /**\n   * Split current Block\n   * 1. Extract content from Caret position to the Block`s end\n   * 2. Insert a new Block below current one with extracted content\n   *\n   * @returns {Block}\n   */\n  public split(): Block {\n    const extractedFragment = this.Editor.Caret.extractFragmentFromCaretPosition();\n    const wrapper = $.make('div');\n\n    wrapper.appendChild(extractedFragment as DocumentFragment);\n\n    /**\n     * @todo make object in accordance with Tool\n     */\n    const data = {\n      text: $.isEmpty(wrapper) ? '' : wrapper.innerHTML,\n    };\n\n    /**\n     * Renew current Block\n     *\n     * @type {Block}\n     */\n    return this.insert({ data });\n  }\n\n  /**\n   * Returns Block by passed index\n   *\n   * If we pass -1 as index, the last block will be returned\n   * There shouldn't be a case when there is no blocks at all — at least one always should exist\n   */\n  public getBlockByIndex(index: -1): Block;\n\n  /**\n   * Returns Block by passed index.\n   *\n   * Could return undefined if there is no block with such index\n   */\n  public getBlockByIndex(index: number): Block | undefined;\n\n  /**\n   * Returns Block by passed index\n   *\n   * @param {number} index - index to get. -1 to get last\n   * @returns {Block}\n   */\n  public getBlockByIndex(index: number): Block | undefined {\n    if (index === -1) {\n      index = this._blocks.length - 1;\n    }\n\n    return this._blocks[index];\n  }\n\n  /**\n   * Returns an index for passed Block\n   *\n   * @param block - block to find index\n   */\n  public getBlockIndex(block: Block): number {\n    return this._blocks.indexOf(block);\n  }\n\n  /**\n   * Returns the Block by passed id\n   *\n   * @param id - id of block to get\n   * @returns {Block}\n   */\n  public getBlockById(id): Block | undefined {\n    return this._blocks.array.find(block => block.id === id);\n  }\n\n  /**\n   * Get Block instance by html element\n   *\n   * @param {Node} element - html element to get Block by\n   */\n  public getBlock(element: HTMLElement): Block | undefined {\n    if (!$.isElement(element) as boolean) {\n      element = element.parentNode as HTMLElement;\n    }\n\n    const nodes = this._blocks.nodes,\n        firstLevelBlock = element.closest(`.${Block.CSS.wrapper}`),\n        index = nodes.indexOf(firstLevelBlock as HTMLElement);\n\n    if (index >= 0) {\n      return this._blocks[index];\n    }\n  }\n\n  /**\n   * 1) Find first-level Block from passed child Node\n   * 2) Mark it as current\n   *\n   * @param {Node} childNode - look ahead from this node.\n   * @returns {Block | undefined} can return undefined in case when the passed child note is not a part of the current editor instance\n   */\n  public setCurrentBlockByChildNode(childNode: Node): Block | undefined {\n    /**\n     * If node is Text TextNode\n     */\n    if (!$.isElement(childNode)) {\n      childNode = childNode.parentNode;\n    }\n\n    const parentFirstLevelBlock = (childNode as HTMLElement).closest(`.${Block.CSS.wrapper}`);\n\n    if (!parentFirstLevelBlock) {\n      return;\n    }\n\n    /**\n     * Support multiple Editor.js instances,\n     * by checking whether the found block belongs to the current instance\n     *\n     * @see {@link Ui#documentTouched}\n     */\n    const editorWrapper = parentFirstLevelBlock.closest(`.${this.Editor.UI.CSS.editorWrapper}`);\n    const isBlockBelongsToCurrentInstance = editorWrapper?.isEqualNode(this.Editor.UI.nodes.wrapper);\n\n    if (!isBlockBelongsToCurrentInstance) {\n      return;\n    }\n\n    /**\n     * Update current Block's index\n     *\n     * @type {number}\n     */\n    this.currentBlockIndex = this._blocks.nodes.indexOf(parentFirstLevelBlock as HTMLElement);\n\n    /**\n     * Update current block active input\n     */\n    this.currentBlock.updateCurrentInput();\n\n    return this.currentBlock;\n  }\n\n  /**\n   * Return block which contents passed node\n   *\n   * @param {Node} childNode - node to get Block by\n   * @returns {Block}\n   */\n  public getBlockByChildNode(childNode: Node): Block | undefined {\n    if (!childNode || childNode instanceof Node === false) {\n      return undefined;\n    }\n\n    /**\n     * If node is Text TextNode\n     */\n    if (!$.isElement(childNode)) {\n      childNode = childNode.parentNode;\n    }\n\n    const firstLevelBlock = (childNode as HTMLElement).closest(`.${Block.CSS.wrapper}`);\n\n    return this.blocks.find((block) => block.holder === firstLevelBlock);\n  }\n\n  /**\n   * Swap Blocks Position\n   *\n   * @param {number} fromIndex - index of first block\n   * @param {number} toIndex - index of second block\n   * @deprecated — use 'move' instead\n   */\n  public swap(fromIndex, toIndex): void {\n    /** Move up current Block */\n    this._blocks.swap(fromIndex, toIndex);\n\n    /** Now actual block moved up so that current block index decreased */\n    this.currentBlockIndex = toIndex;\n  }\n\n  /**\n   * Move a block to a new index\n   *\n   * @param {number} toIndex - index where to move Block\n   * @param {number} fromIndex - index of Block to move\n   */\n  public move(toIndex, fromIndex = this.currentBlockIndex): void {\n    // make sure indexes are valid and within a valid range\n    if (isNaN(toIndex) || isNaN(fromIndex)) {\n      _.log(`Warning during 'move' call: incorrect indices provided.`, 'warn');\n\n      return;\n    }\n\n    if (!this.validateIndex(toIndex) || !this.validateIndex(fromIndex)) {\n      _.log(`Warning during 'move' call: indices cannot be lower than 0 or greater than the amount of blocks.`, 'warn');\n\n      return;\n    }\n\n    /** Move up current Block */\n    this._blocks.move(toIndex, fromIndex);\n\n    /** Now actual block moved so that current block index changed */\n    this.currentBlockIndex = toIndex;\n\n    /**\n     * Force call of didMutated event on Block movement\n     */\n    this.blockDidMutated(BlockMovedMutationType, this.currentBlock, {\n      fromIndex,\n      toIndex,\n    });\n  }\n\n  /**\n   * Converts passed Block to the new Tool\n   * Uses Conversion Config\n   *\n   * @param blockToConvert - Block that should be converted\n   * @param targetToolName - name of the Tool to convert to\n   * @param blockDataOverrides - optional new Block data overrides\n   */\n  public async convert(blockToConvert: Block, targetToolName: string, blockDataOverrides?: BlockToolData): Promise<Block> {\n    /**\n     * At first, we get current Block data\n     */\n    const savedBlock = await blockToConvert.save();\n\n    if (!savedBlock) {\n      throw new Error('Could not convert Block. Failed to extract original Block data.');\n    }\n\n    /**\n     * Getting a class of the replacing Tool\n     */\n    const replacingTool = this.Editor.Tools.blockTools.get(targetToolName);\n\n    if (!replacingTool) {\n      throw new Error(`Could not convert Block. Tool «${targetToolName}» not found.`);\n    }\n\n    /**\n     * Using Conversion Config \"export\" we get a stringified version of the Block data\n     */\n    const exportedData = await blockToConvert.exportDataAsString();\n\n    /**\n     * Clean exported data with replacing sanitizer config\n     */\n    const cleanData: string = clean(\n      exportedData,\n      replacingTool.sanitizeConfig\n    );\n\n    /**\n     * Now using Conversion Config \"import\" we compose a new Block data\n     */\n    let newBlockData = convertStringToBlockData(cleanData, replacingTool.conversionConfig, replacingTool.settings);\n\n    /**\n     * Optional data overrides.\n     * Used for example, by the Multiple Toolbox Items feature, where a single Tool provides several Toolbox items with \"data\" overrides\n     */\n    if (blockDataOverrides) {\n      newBlockData = Object.assign(newBlockData, blockDataOverrides);\n    }\n\n    return this.replace(blockToConvert, replacingTool.name, newBlockData);\n  }\n\n  /**\n   * Sets current Block Index -1 which means unknown\n   * and clear highlights\n   */\n  public unsetCurrentBlock(): void {\n    this.currentBlockIndex = -1;\n  }\n\n  /**\n   * Clears Editor\n   *\n   * @param {boolean} needToAddDefaultBlock - 1) in internal calls (for example, in api.blocks.render)\n   *                                             we don't need to add an empty default block\n   *                                        2) in api.blocks.clear we should add empty block\n   */\n  public async clear(needToAddDefaultBlock = false): Promise<void> {\n    const queue = new PromiseQueue();\n\n    // Create a copy of the blocks array to avoid issues with array modification during iteration\n    const blocksToRemove = [...this.blocks];\n    \n    blocksToRemove.forEach((block) => {\n      queue.add(async () => {\n        await this.removeBlock(block, false);\n      });\n    });\n\n    await queue.completed;\n\n    this.unsetCurrentBlock();\n\n    if (needToAddDefaultBlock) {\n      this.insert();\n    }\n\n    /**\n     * Add empty modifier\n     */\n    this.Editor.UI.checkEmptiness();\n  }\n\n  /**\n   * Cleans up all the block tools' resources\n   * This is called when editor is destroyed\n   */\n  public async destroy(): Promise<void> {\n    await Promise.all(this.blocks.map((block) => {\n      return block.destroy();\n    }));\n  }\n\n  /**\n   * Bind Block events\n   *\n   * @param {Block} block - Block to which event should be bound\n   */\n  private bindBlockEvents(block: Block): void {\n    const { BlockEvents } = this.Editor;\n\n    this.readOnlyMutableListeners.on(block.holder, 'keydown', (event: KeyboardEvent) => {\n      BlockEvents.keydown(event);\n    });\n\n    this.readOnlyMutableListeners.on(block.holder, 'keyup', (event: KeyboardEvent) => {\n      BlockEvents.keyup(event);\n    });\n\n    this.readOnlyMutableListeners.on(block.holder, 'dragover', (event: DragEvent) => {\n      BlockEvents.dragOver(event);\n    });\n\n    this.readOnlyMutableListeners.on(block.holder, 'dragleave', (event: DragEvent) => {\n      BlockEvents.dragLeave(event);\n    });\n\n    block.on('didMutated', (affectedBlock: Block) => {\n      return this.blockDidMutated(BlockChangedMutationType, affectedBlock, {\n        index: this.getBlockIndex(affectedBlock),\n      });\n    });\n  }\n\n  /**\n   * Disable mutable handlers and bindings\n   */\n  private disableModuleBindings(): void {\n    this.readOnlyMutableListeners.clearAll();\n  }\n\n  /**\n   * Enables all module handlers and bindings for all Blocks\n   */\n  private enableModuleBindings(): void {\n    /** Cut event */\n    this.readOnlyMutableListeners.on(\n      document,\n      'cut',\n      (e: ClipboardEvent) => this.Editor.BlockEvents.handleCommandX(e)\n    );\n\n    this.blocks.forEach((block: Block) => {\n      this.bindBlockEvents(block);\n    });\n  }\n\n  /**\n   * Validates that the given index is not lower than 0 or higher than the amount of blocks\n   *\n   * @param {number} index - index of blocks array to validate\n   * @returns {boolean}\n   */\n  private validateIndex(index: number): boolean {\n    return !(index < 0 || index >= this._blocks.length);\n  }\n\n  /**\n   * Block mutation callback\n   *\n   * @param mutationType - what happened with block\n   * @param block - mutated block\n   * @param detailData - additional data to pass with change event\n   */\n  private blockDidMutated<Type extends BlockMutationType>(mutationType: Type, block: Block, detailData: BlockMutationEventDetailWithoutTarget<Type>): Block {\n    const event = new CustomEvent(mutationType, {\n      detail: {\n        target: new BlockAPI(block),\n        ...detailData as BlockMutationEventDetailWithoutTarget<Type>,\n      },\n    });\n\n    this.eventsDispatcher.emit(BlockChanged, {\n      event: event as BlockMutationEventMap[Type],\n    });\n\n    return block;\n  }\n}\n\n/**\n * Type alias for Block Mutation event without 'target' field, used in 'blockDidMutated' method\n */\ntype BlockMutationEventDetailWithoutTarget<Type extends BlockMutationType> = Omit<BlockMutationEventMap[Type]['detail'], 'target'>;\n"
  },
  {
    "path": "src/components/modules/blockSelection.ts",
    "content": "/**\n * @class BlockSelection\n * @classdesc Manages Block selection with shortcut CMD+A\n * @module BlockSelection\n * @version 1.0.0\n */\nimport Module from '../__module';\nimport type Block from '../block';\nimport * as _ from '../utils';\nimport $ from '../dom';\nimport Shortcuts from '../utils/shortcuts';\n\nimport SelectionUtils from '../selection';\nimport type { SanitizerConfig } from '../../../types/configs';\nimport { clean } from '../utils/sanitizer';\n\n/**\n *\n */\nexport default class BlockSelection extends Module {\n  /**\n   * Sometimes .anyBlockSelected can be called frequently,\n   * for example at ui@selectionChange (to clear native browser selection in CBS)\n   * We use cache to prevent multiple iterations through all the blocks\n   *\n   * @private\n   */\n  private anyBlockSelectedCache: boolean | null = null;\n\n  /**\n   * Sanitizer Config\n   *\n   * @returns {SanitizerConfig}\n   */\n  private get sanitizerConfig(): SanitizerConfig {\n    return {\n      p: {},\n      h1: {},\n      h2: {},\n      h3: {},\n      h4: {},\n      h5: {},\n      h6: {},\n      ol: {},\n      ul: {},\n      li: {},\n      br: true,\n      img: {\n        src: true,\n        width: true,\n        height: true,\n      },\n      a: {\n        href: true,\n      },\n      b: {},\n      i: {},\n      u: {},\n    };\n  }\n\n  /**\n   * Flag that identifies all Blocks selection\n   *\n   * @returns {boolean}\n   */\n  public get allBlocksSelected(): boolean {\n    const { BlockManager } = this.Editor;\n\n    return BlockManager.blocks.every((block) => block.selected === true);\n  }\n\n  /**\n   * Set selected all blocks\n   *\n   * @param {boolean} state - state to set\n   */\n  public set allBlocksSelected(state: boolean) {\n    const { BlockManager } = this.Editor;\n\n    BlockManager.blocks.forEach((block) => {\n      block.selected = state;\n    });\n\n    this.clearCache();\n  }\n\n  /**\n   * Flag that identifies any Block selection\n   *\n   * @returns {boolean}\n   */\n  public get anyBlockSelected(): boolean {\n    const { BlockManager } = this.Editor;\n\n    if (this.anyBlockSelectedCache === null) {\n      this.anyBlockSelectedCache = BlockManager.blocks.some((block) => block.selected === true);\n    }\n\n    return this.anyBlockSelectedCache;\n  }\n\n  /**\n   * Return selected Blocks array\n   *\n   * @returns {Block[]}\n   */\n  public get selectedBlocks(): Block[] {\n    return this.Editor.BlockManager.blocks.filter((block: Block) => block.selected);\n  }\n\n  /**\n   * Flag used to define block selection\n   * First CMD+A defines it as true and then second CMD+A selects all Blocks\n   *\n   * @type {boolean}\n   */\n  private needToSelectAll = false;\n\n  /**\n   * Flag used to define native input selection\n   * In this case we allow double CMD+A to select Block\n   *\n   * @type {boolean}\n   */\n  private nativeInputSelected = false;\n\n  /**\n   * Flag identifies any input selection\n   * That means we can select whole Block\n   *\n   * @type {boolean}\n   */\n  private readyToBlockSelection = false;\n\n  /**\n   * SelectionUtils instance\n   *\n   * @type {SelectionUtils}\n   */\n  private selection: SelectionUtils;\n\n  /**\n   * Module Preparation\n   * Registers Shortcuts CMD+A and CMD+C\n   * to select all and copy them\n   */\n  public prepare(): void {\n    this.selection = new SelectionUtils();\n\n    /**\n     * CMD/CTRL+A selection shortcut\n     */\n    Shortcuts.add({\n      name: 'CMD+A',\n      handler: (event) => {\n        const { BlockManager, ReadOnly } = this.Editor;\n\n        /**\n         * We use Editor's Block selection on CMD+A ShortCut instead of Browsers\n         */\n        if (ReadOnly.isEnabled) {\n          event.preventDefault();\n          this.selectAllBlocks();\n\n          return;\n        }\n\n        /**\n         * When one page consist of two or more EditorJS instances\n         * Shortcut module tries to handle all events.\n         * Thats why Editor's selection works inside the target Editor, but\n         * for others error occurs because nothing to select.\n         *\n         * Prevent such actions if focus is not inside the Editor\n         */\n        if (!BlockManager.currentBlock) {\n          return;\n        }\n\n        this.handleCommandA(event);\n      },\n      on: this.Editor.UI.nodes.redactor,\n    });\n  }\n\n  /**\n   * Toggle read-only state\n   *\n   *  - Remove all ranges\n   *  - Unselect all Blocks\n   */\n  public toggleReadOnly(): void {\n    SelectionUtils.get()\n      .removeAllRanges();\n\n    this.allBlocksSelected = false;\n  }\n\n  /**\n   * Remove selection of Block\n   *\n   * @param {number?} index - Block index according to the BlockManager's indexes\n   */\n  public unSelectBlockByIndex(index?): void {\n    const { BlockManager } = this.Editor;\n\n    let block;\n\n    if (isNaN(index)) {\n      block = BlockManager.currentBlock;\n    } else {\n      block = BlockManager.getBlockByIndex(index);\n    }\n\n    block.selected = false;\n\n    this.clearCache();\n  }\n\n  /**\n   * Clear selection from Blocks\n   *\n   * @param {Event} reason - event caused clear of selection\n   * @param {boolean} restoreSelection - if true, restore saved selection\n   */\n  public clearSelection(reason?: Event, restoreSelection = false): void {\n    const { BlockManager, Caret, RectangleSelection } = this.Editor;\n\n    this.needToSelectAll = false;\n    this.nativeInputSelected = false;\n    this.readyToBlockSelection = false;\n\n    const isKeyboard = reason && (reason instanceof KeyboardEvent);\n    const isPrintableKey = isKeyboard && _.isPrintableKey((reason as KeyboardEvent).keyCode);\n\n    /**\n     * If reason caused clear of the selection was printable key and any block is selected,\n     * remove selected blocks and insert pressed key\n     */\n    if (this.anyBlockSelected && isKeyboard && isPrintableKey && !SelectionUtils.isSelectionExists) {\n      const indexToInsert = BlockManager.removeSelectedBlocks();\n\n      BlockManager.insertDefaultBlockAtIndex(indexToInsert, true);\n      Caret.setToBlock(BlockManager.currentBlock);\n      _.delay(() => {\n        const eventKey = (reason as KeyboardEvent).key;\n\n        /**\n         * If event.key length >1 that means key is special (e.g. Enter or Dead or Unidentified).\n         * So we use empty string\n         *\n         * @see https://developer.mozilla.org/ru/docs/Web/API/KeyboardEvent/key\n         */\n        Caret.insertContentAtCaretPosition(eventKey.length > 1 ? '' : eventKey);\n      // eslint-disable-next-line @typescript-eslint/no-magic-numbers\n      }, 20)();\n    }\n\n    this.Editor.CrossBlockSelection.clear(reason);\n\n    if (!this.anyBlockSelected || RectangleSelection.isRectActivated()) {\n      this.Editor.RectangleSelection.clearSelection();\n\n      return;\n    }\n\n    /**\n     * Restore selection when Block is already selected\n     * but someone tries to write something.\n     */\n    if (restoreSelection) {\n      this.selection.restore();\n    }\n\n    /** Now all blocks cleared */\n    this.allBlocksSelected = false;\n  }\n\n  /**\n   * Reduce each Block and copy its content\n   *\n   * @param {ClipboardEvent} e - copy/cut event\n   * @returns {Promise<void>}\n   */\n  public copySelectedBlocks(e: ClipboardEvent): Promise<void> {\n    /**\n     * Prevent default copy\n     */\n    e.preventDefault();\n\n    const fakeClipboard = $.make('div');\n\n    this.selectedBlocks.forEach((block) => {\n      /**\n       * Make <p> tag that holds clean HTML\n       */\n      const cleanHTML = clean(block.holder.innerHTML, this.sanitizerConfig);\n      const fragment = $.make('p');\n\n      fragment.innerHTML = cleanHTML;\n      fakeClipboard.appendChild(fragment);\n    });\n\n    const textPlain = Array.from(fakeClipboard.childNodes).map((node) => node.textContent)\n      .join('\\n\\n');\n    const textHTML = fakeClipboard.innerHTML;\n\n    e.clipboardData.setData('text/plain', textPlain);\n    e.clipboardData.setData('text/html', textHTML);\n\n    return Promise\n      .all(this.selectedBlocks.map((block) => block.save()))\n      .then(savedData => {\n        try {\n          e.clipboardData.setData(this.Editor.Paste.MIME_TYPE, JSON.stringify(savedData));\n        } catch (err) {\n          // In Firefox we can't set data in async function\n        }\n      });\n  }\n\n  /**\n   * Select Block by its index\n   *\n   * @param {number?} index - Block index according to the BlockManager's indexes\n   */\n  public selectBlockByIndex(index: number): void {\n    const { BlockManager } = this.Editor;\n\n    const block = BlockManager.getBlockByIndex(index);\n\n    if (block === undefined) {\n      return;\n    }\n\n    this.selectBlock(block);\n  }\n\n  /**\n   * Select passed Block\n   *\n   * @param {Block} block - Block to select\n   */\n  public selectBlock(block: Block): void {\n    /** Save selection */\n    this.selection.save();\n    SelectionUtils.get()\n      .removeAllRanges();\n\n    block.selected = true;\n\n    this.clearCache();\n\n    /** close InlineToolbar when we selected any Block */\n    this.Editor.InlineToolbar.close();\n  }\n\n  /**\n   * Remove selection from passed Block\n   *\n   * @param {Block} block - Block to unselect\n   */\n  public unselectBlock(block: Block): void {\n    block.selected = false;\n\n    this.clearCache();\n  }\n\n  /**\n   * Clear anyBlockSelected cache\n   */\n  public clearCache(): void {\n    this.anyBlockSelectedCache = null;\n  }\n\n  /**\n   * Module destruction\n   * De-registers Shortcut CMD+A\n   */\n  public destroy(): void {\n    /** Selection shortcut */\n    Shortcuts.remove(this.Editor.UI.nodes.redactor, 'CMD+A');\n  }\n\n  /**\n   * First CMD+A selects all input content by native behaviour,\n   * next CMD+A keypress selects all blocks\n   *\n   * @param {KeyboardEvent} event - keyboard event\n   */\n  private handleCommandA(event: KeyboardEvent): void {\n    this.Editor.RectangleSelection.clearSelection();\n\n    /** allow default selection on native inputs */\n    if ($.isNativeInput(event.target) && !this.readyToBlockSelection) {\n      this.readyToBlockSelection = true;\n\n      return;\n    }\n\n    const workingBlock = this.Editor.BlockManager.getBlock(event.target as HTMLElement);\n    const inputs = workingBlock.inputs;\n\n    /**\n     * If Block has more than one editable element allow native selection\n     * Second cmd+a will select whole Block\n     */\n    if (inputs.length > 1 && !this.readyToBlockSelection) {\n      this.readyToBlockSelection = true;\n\n      return;\n    }\n\n    if (inputs.length === 1 && !this.needToSelectAll) {\n      this.needToSelectAll = true;\n\n      return;\n    }\n\n    if (this.needToSelectAll) {\n      /**\n       * Prevent default selection\n       */\n      event.preventDefault();\n\n      this.selectAllBlocks();\n\n      /**\n       * Disable any selection after all Blocks selected\n       */\n      this.needToSelectAll = false;\n      this.readyToBlockSelection = false;\n    } else if (this.readyToBlockSelection) {\n      /**\n       * prevent default selection when we use custom selection\n       */\n      event.preventDefault();\n\n      /**\n       * select working Block\n       */\n      this.selectBlock(workingBlock);\n\n      /**\n       * Enable all Blocks selection if current Block is selected\n       */\n      this.needToSelectAll = true;\n    }\n  }\n\n  /**\n   * Select All Blocks\n   * Each Block has selected setter that makes Block copyable\n   */\n  private selectAllBlocks(): void {\n    /**\n     * Save selection\n     * Will be restored when closeSelection fired\n     */\n    this.selection.save();\n\n    /**\n     * Remove Ranges from Selection\n     */\n    SelectionUtils.get()\n      .removeAllRanges();\n\n    this.allBlocksSelected = true;\n\n    /** close InlineToolbar if we selected all Blocks */\n    this.Editor.InlineToolbar.close();\n  }\n}\n"
  },
  {
    "path": "src/components/modules/caret.ts",
    "content": "import Selection from '../selection';\nimport Module from '../__module';\nimport type Block from '../block';\nimport * as caretUtils from '../utils/caret';\nimport $  from '../dom';\n\n/**\n * Caret\n * Contains methods for working Caret\n *\n * @todo get rid of this module and separate it for utility functions\n */\nexport default class Caret extends Module {\n  /**\n   * Allowed caret positions in input\n   *\n   * @static\n   * @returns {{START: string, END: string, DEFAULT: string}}\n   */\n  public get positions(): {START: string; END: string; DEFAULT: string} {\n    return {\n      START: 'start',\n      END: 'end',\n      DEFAULT: 'default',\n    };\n  }\n\n  /**\n   * Elements styles that can be useful for Caret Module\n   */\n  private static get CSS(): {shadowCaret: string} {\n    return {\n      shadowCaret: 'cdx-shadow-caret',\n    };\n  }\n\n  /**\n   * Method gets Block instance and puts caret to the text node with offset\n   * There two ways that method applies caret position:\n   *   - first found text node: sets at the beginning, but you can pass an offset\n   *   - last found text node: sets at the end of the node. Also, you can customize the behaviour\n   *\n   * @param {Block} block - Block class\n   * @param {string} position - position where to set caret.\n   *                            If default - leave default behaviour and apply offset if it's passed\n   * @param {number} offset - caret offset regarding to the block content\n   */\n  public setToBlock(block: Block, position: string = this.positions.DEFAULT, offset = 0): void {\n    const { BlockManager, BlockSelection } = this.Editor;\n\n    /**\n     * Clear previous selection since we possible will select the new Block\n     */\n    BlockSelection.clearSelection();\n\n    /**\n     * If Block is not focusable, just select (highlight) it\n     */\n    if (!block.focusable) {\n      /**\n       * Hide current cursor\n       */\n      window.getSelection()?.removeAllRanges();\n\n      /**\n       * Highlight Block\n       */\n      BlockSelection.selectBlock(block);\n      BlockManager.currentBlock = block;\n\n      return;\n    }\n\n    let element;\n\n    switch (position) {\n      case this.positions.START:\n        element = block.firstInput;\n        break;\n      case this.positions.END:\n        element = block.lastInput;\n        break;\n      default:\n        element = block.currentInput;\n    }\n\n    if (!element) {\n      return;\n    }\n\n    let nodeToSet: Node;\n    let offsetToSet = offset;\n\n    if (position === this.positions.START) {\n      nodeToSet = $.getDeepestNode(element, false) as Node;\n      offsetToSet = 0;\n    } else if (position === this.positions.END) {\n      nodeToSet = $.getDeepestNode(element, true) as Node;\n      offsetToSet = $.getContentLength(nodeToSet);\n    } else {\n      const { node, offset: nodeOffset } = $.getNodeByOffset(element, offset);\n\n      if (node) {\n        nodeToSet = node;\n        offsetToSet = nodeOffset;\n      } else { // case for empty block's input\n        nodeToSet = $.getDeepestNode(element, false) as Node;\n        offsetToSet = 0;\n      }\n    }\n\n    this.set(nodeToSet as HTMLElement, offsetToSet);\n\n    BlockManager.setCurrentBlockByChildNode(block.holder);\n    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n    BlockManager.currentBlock!.currentInput = element;\n  }\n\n  /**\n   * Set caret to the current input of current Block.\n   *\n   * @param {HTMLElement} input - input where caret should be set\n   * @param {string} position - position of the caret.\n   *                            If default - leave default behaviour and apply offset if it's passed\n   * @param {number} offset - caret offset regarding to the text node\n   */\n  public setToInput(input: HTMLElement, position: string = this.positions.DEFAULT, offset = 0): void {\n    const { currentBlock } = this.Editor.BlockManager;\n    const nodeToSet = $.getDeepestNode(input);\n\n    switch (position) {\n      case this.positions.START:\n        this.set(nodeToSet as HTMLElement, 0);\n        break;\n\n      case this.positions.END:\n        this.set(nodeToSet as HTMLElement, $.getContentLength(nodeToSet));\n        break;\n\n      default:\n        if (offset) {\n          this.set(nodeToSet as HTMLElement, offset);\n        }\n    }\n\n    currentBlock.currentInput = input;\n  }\n\n  /**\n   * Creates Document Range and sets caret to the element with offset\n   *\n   * @param {HTMLElement} element - target node.\n   * @param {number} offset - offset\n   */\n  public set(element: HTMLElement, offset = 0): void {\n    const scrollOffset = 30;\n    const { top, bottom } = Selection.setCursor(element, offset);\n    const { innerHeight } = window;\n\n    /**\n     * If new cursor position is not visible, scroll to it\n     */\n    if (top < 0) {\n      window.scrollBy(0, top - scrollOffset);\n    } else if (bottom > innerHeight) {\n      window.scrollBy(0, bottom - innerHeight + scrollOffset);\n    }\n  }\n\n  /**\n   * Set Caret to the last Block\n   * If last block is not empty, append another empty block\n   */\n  public setToTheLastBlock(): void {\n    const lastBlock = this.Editor.BlockManager.lastBlock;\n\n    if (!lastBlock) {\n      return;\n    }\n\n    /**\n     * If last block is empty and it is an defaultBlock, set to that.\n     * Otherwise, append new empty block and set to that\n     */\n    if (lastBlock.tool.isDefault && lastBlock.isEmpty) {\n      this.setToBlock(lastBlock);\n    } else {\n      const newBlock = this.Editor.BlockManager.insertAtEnd();\n\n      this.setToBlock(newBlock);\n    }\n  }\n\n  /**\n   * Extract content fragment of current Block from Caret position to the end of the Block\n   */\n  public extractFragmentFromCaretPosition(): void|DocumentFragment {\n    const selection = Selection.get();\n\n    if (selection.rangeCount) {\n      const selectRange = selection.getRangeAt(0);\n      const currentBlockInput = this.Editor.BlockManager.currentBlock.currentInput;\n\n      selectRange.deleteContents();\n\n      if (currentBlockInput) {\n        if ($.isNativeInput(currentBlockInput)) {\n          /**\n           * If input is native text input we need to use it's value\n           * Text before the caret stays in the input,\n           * while text after the caret is returned as a fragment to be inserted after the block.\n           */\n          const input = currentBlockInput as HTMLInputElement | HTMLTextAreaElement;\n          const newFragment = document.createDocumentFragment();\n\n          const inputRemainingText = input.value.substring(0, input.selectionStart);\n          const fragmentText = input.value.substring(input.selectionStart);\n\n          newFragment.textContent = fragmentText;\n          input.value = inputRemainingText;\n\n          return newFragment;\n        } else {\n          const range = selectRange.cloneRange();\n\n          range.selectNodeContents(currentBlockInput);\n          range.setStart(selectRange.endContainer, selectRange.endOffset);\n\n          return range.extractContents();\n        }\n      }\n    }\n  }\n\n  /**\n   * Set's caret to the next Block or Tool`s input\n   * Before moving caret, we should check if caret position is at the end of Plugins node\n   * Using {@link Dom#getDeepestNode} to get a last node and match with current selection\n   *\n   * @param {boolean} force - pass true to skip check for caret position\n   */\n  public navigateNext(force = false): boolean {\n    const { BlockManager } = this.Editor;\n    const { currentBlock, nextBlock } = BlockManager;\n\n    if (currentBlock === undefined) {\n      return false;\n    }\n\n    const { nextInput, currentInput } = currentBlock;\n    const isAtEnd = currentInput !== undefined ? caretUtils.isCaretAtEndOfInput(currentInput) : undefined;\n\n    let blockToNavigate = nextBlock;\n\n    /**\n     * We should jump to the next block if:\n     * - 'force' is true (Tab-navigation)\n     * - caret is at the end of the current block\n     * - block does not contain any inputs (e.g. to allow go next when Delimiter is focused)\n     */\n    const navigationAllowed = force || isAtEnd || !currentBlock.focusable;\n\n    /** If next Tool`s input exists, focus on it. Otherwise set caret to the next Block */\n    if (nextInput && navigationAllowed) {\n      this.setToInput(nextInput, this.positions.START);\n\n      return true;\n    }\n\n    if (blockToNavigate === null) {\n      /**\n       * This code allows to exit from the last non-initial tool:\n       * https://github.com/codex-team/editor.js/issues/1103\n       */\n\n      /**\n       * 1. If there is a last block and it is default, do nothing\n       * 2. If there is a last block and it is non-default --> and caret not at the end <--, do nothing\n       *    (https://github.com/codex-team/editor.js/issues/1414)\n       */\n      if (currentBlock.tool.isDefault || !navigationAllowed) {\n        return false;\n      }\n\n      /**\n       * If there is no nextBlock, but currentBlock is not default,\n       * insert new default block at the end and navigate to it\n       */\n      blockToNavigate = BlockManager.insertAtEnd() as Block;\n    }\n\n    if (navigationAllowed) {\n      this.setToBlock(blockToNavigate, this.positions.START);\n\n      return true;\n    }\n\n    return false;\n  }\n\n  /**\n   * Set's caret to the previous Tool`s input or Block\n   * Before moving caret, we should check if caret position is start of the Plugins node\n   * Using {@link Dom#getDeepestNode} to get a last node and match with current selection\n   *\n   * @param {boolean} force - pass true to skip check for caret position\n   */\n  public navigatePrevious(force = false): boolean {\n    const { currentBlock, previousBlock } = this.Editor.BlockManager;\n\n    if (!currentBlock) {\n      return false;\n    }\n\n    const { previousInput, currentInput } = currentBlock;\n\n    /**\n     * We should jump to the previous block if:\n     * - 'force' is true (Tab-navigation)\n     * - caret is at the start of the current block\n     * - block does not contain any inputs (e.g. to allow go back when Delimiter is focused)\n     */\n    const caretAtStart = currentInput !== undefined ? caretUtils.isCaretAtStartOfInput(currentInput) : undefined;\n    const navigationAllowed = force || caretAtStart || !currentBlock.focusable;\n\n    /** If previous Tool`s input exists, focus on it. Otherwise set caret to the previous Block */\n    if (previousInput && navigationAllowed) {\n      this.setToInput(previousInput, this.positions.END);\n\n      return true;\n    }\n\n    if (previousBlock !== null && navigationAllowed) {\n      this.setToBlock(previousBlock as Block, this.positions.END);\n\n      return true;\n    }\n\n    return false;\n  }\n\n  /**\n   * Inserts shadow element after passed element where caret can be placed\n   *\n   * @param {Element} element - element after which shadow caret should be inserted\n   */\n  public createShadow(element: Element): void {\n    const shadowCaret = document.createElement('span');\n\n    shadowCaret.classList.add(Caret.CSS.shadowCaret);\n    element.insertAdjacentElement('beforeend', shadowCaret);\n  }\n\n  /**\n   * Restores caret position\n   *\n   * @param {HTMLElement} element - element where caret should be restored\n   */\n  public restoreCaret(element: HTMLElement): void {\n    const shadowCaret = element.querySelector(`.${Caret.CSS.shadowCaret}`);\n\n    if (!shadowCaret) {\n      return;\n    }\n\n    /**\n     * After we set the caret to the required place\n     * we need to clear shadow caret\n     *\n     * - make new range\n     * - select shadowed span\n     * - use extractContent to remove it from DOM\n     */\n    const sel = new Selection();\n\n    sel.expandToTag(shadowCaret as HTMLElement);\n\n    const newRange = document.createRange();\n\n    newRange.selectNode(shadowCaret);\n    newRange.extractContents();\n  }\n\n  /**\n   * Inserts passed content at caret position\n   *\n   * @param {string} content - content to insert\n   */\n  public insertContentAtCaretPosition(content: string): void {\n    const fragment = document.createDocumentFragment();\n    const wrapper = document.createElement('div');\n    const selection = Selection.get();\n    const range = Selection.range;\n\n    wrapper.innerHTML = content;\n\n    Array.from(wrapper.childNodes).forEach((child: Node) => fragment.appendChild(child));\n\n    /**\n     * If there is no child node, append empty one\n     */\n    if (fragment.childNodes.length === 0) {\n      fragment.appendChild(new Text());\n    }\n\n    const lastChild = fragment.lastChild as ChildNode;\n\n    range.deleteContents();\n    range.insertNode(fragment);\n\n    /** Cross-browser caret insertion */\n    const newRange = document.createRange();\n\n    const nodeToSetCaret = lastChild.nodeType === Node.TEXT_NODE ? lastChild : lastChild.firstChild;\n\n    if (nodeToSetCaret !== null && nodeToSetCaret.textContent !== null) {\n      newRange.setStart(nodeToSetCaret, nodeToSetCaret.textContent.length);\n    }\n\n    selection.removeAllRanges();\n    selection.addRange(newRange);\n  }\n}\n"
  },
  {
    "path": "src/components/modules/crossBlockSelection.ts",
    "content": "import Module from '../__module';\nimport type Block from '../block';\nimport SelectionUtils from '../selection';\nimport * as _ from '../utils';\n\n/**\n *\n */\nexport default class CrossBlockSelection extends Module {\n  /**\n   * Block where selection is started\n   */\n  private firstSelectedBlock: Block;\n\n  /**\n   * Last selected Block\n   */\n  private lastSelectedBlock: Block;\n\n  /**\n   * Module preparation\n   *\n   * @returns {Promise}\n   */\n  public async prepare(): Promise<void> {\n    this.listeners.on(document, 'mousedown', (event: MouseEvent) => {\n      this.enableCrossBlockSelection(event);\n    });\n  }\n\n  /**\n   * Sets up listeners\n   *\n   * @param {MouseEvent} event - mouse down event\n   */\n  public watchSelection(event: MouseEvent): void {\n    if (event.button !== _.mouseButtons.LEFT) {\n      return;\n    }\n\n    const { BlockManager } = this.Editor;\n\n    this.firstSelectedBlock = BlockManager.getBlock(event.target as HTMLElement);\n    this.lastSelectedBlock = this.firstSelectedBlock;\n\n    this.listeners.on(document, 'mouseover', this.onMouseOver);\n    this.listeners.on(document, 'mouseup', this.onMouseUp);\n  }\n\n  /**\n   * Return boolean is cross block selection started:\n   * there should be at least 2 selected blocks\n   */\n  public get isCrossBlockSelectionStarted(): boolean {\n    return !!this.firstSelectedBlock && !!this.lastSelectedBlock && this.firstSelectedBlock !== this.lastSelectedBlock;\n  }\n\n  /**\n   * Change selection state of the next Block\n   * Used for CBS via Shift + arrow keys\n   *\n   * @param {boolean} next - if true, toggle next block. Previous otherwise\n   */\n  public toggleBlockSelectedState(next = true): void {\n    const { BlockManager, BlockSelection } = this.Editor;\n\n    if (!this.lastSelectedBlock) {\n      this.lastSelectedBlock = this.firstSelectedBlock = BlockManager.currentBlock;\n    }\n\n    if (this.firstSelectedBlock === this.lastSelectedBlock) {\n      this.firstSelectedBlock.selected = true;\n\n      BlockSelection.clearCache();\n      SelectionUtils.get().removeAllRanges();\n    }\n\n    const nextBlockIndex = BlockManager.blocks.indexOf(this.lastSelectedBlock) + (next ? 1 : -1);\n    const nextBlock = BlockManager.blocks[nextBlockIndex];\n\n    if (!nextBlock) {\n      return;\n    }\n\n    if (this.lastSelectedBlock.selected !== nextBlock.selected) {\n      nextBlock.selected = true;\n\n      BlockSelection.clearCache();\n    } else {\n      this.lastSelectedBlock.selected = false;\n\n      BlockSelection.clearCache();\n    }\n\n    this.lastSelectedBlock = nextBlock;\n\n    /** close InlineToolbar when Blocks selected */\n    this.Editor.InlineToolbar.close();\n\n    nextBlock.holder.scrollIntoView({\n      block: 'nearest',\n    });\n  }\n\n  /**\n   * Clear saved state\n   *\n   * @param {Event} reason - event caused clear of selection\n   */\n  public clear(reason?: Event): void {\n    const { BlockManager, BlockSelection, Caret } = this.Editor;\n    const fIndex = BlockManager.blocks.indexOf(this.firstSelectedBlock);\n    const lIndex = BlockManager.blocks.indexOf(this.lastSelectedBlock);\n\n    if (BlockSelection.anyBlockSelected && fIndex > -1 && lIndex > -1) {\n      if (reason && reason instanceof KeyboardEvent) {\n        /**\n         * Set caret depending on pressed key if pressed key is an arrow.\n         */\n        switch (reason.keyCode) {\n          case _.keyCodes.DOWN:\n          case _.keyCodes.RIGHT:\n            Caret.setToBlock(BlockManager.blocks[Math.max(fIndex, lIndex)], Caret.positions.END);\n            break;\n\n          case _.keyCodes.UP:\n          case _.keyCodes.LEFT:\n            Caret.setToBlock(BlockManager.blocks[Math.min(fIndex, lIndex)], Caret.positions.START);\n            break;\n          default:\n            Caret.setToBlock(BlockManager.blocks[Math.max(fIndex, lIndex)], Caret.positions.END);\n        }\n      }\n    }\n\n    this.firstSelectedBlock = this.lastSelectedBlock = null;\n  }\n\n  /**\n   * Enables Cross Block Selection\n   *\n   * @param {MouseEvent} event - mouse down event\n   */\n  private enableCrossBlockSelection(event: MouseEvent): void {\n    const { UI } = this.Editor;\n\n    /**\n     * Each mouse down on must disable selectAll state\n     */\n    if (!SelectionUtils.isCollapsed) {\n      this.Editor.BlockSelection.clearSelection(event);\n    }\n\n    /**\n     * If mouse down is performed inside the editor, we should watch CBS\n     */\n    if (UI.nodes.redactor.contains(event.target as Node)) {\n      this.watchSelection(event);\n    } else {\n      /**\n       * Otherwise, clear selection\n       */\n      this.Editor.BlockSelection.clearSelection(event);\n    }\n  }\n\n  /**\n   * Mouse up event handler.\n   * Removes the listeners\n   */\n  private onMouseUp = (): void => {\n    this.listeners.off(document, 'mouseover', this.onMouseOver);\n    this.listeners.off(document, 'mouseup', this.onMouseUp);\n  };\n\n  /**\n   * Mouse over event handler\n   * Gets target and related blocks and change selected state for blocks in between\n   *\n   * @param {MouseEvent} event - mouse over event\n   */\n  private onMouseOver = (event: MouseEvent): void => {\n    const { BlockManager, BlockSelection } = this.Editor;\n\n    /**\n     * Probably, editor is not initialized yet\n     */\n    if (event.relatedTarget === null && event.target === null) {\n      return;\n    }\n\n    const relatedBlock = BlockManager.getBlockByChildNode(event.relatedTarget as Node) || this.lastSelectedBlock;\n    const targetBlock = BlockManager.getBlockByChildNode(event.target as Node);\n\n    if (!relatedBlock || !targetBlock) {\n      return;\n    }\n\n    if (targetBlock === relatedBlock) {\n      return;\n    }\n\n    if (relatedBlock === this.firstSelectedBlock) {\n      SelectionUtils.get().removeAllRanges();\n\n      relatedBlock.selected = true;\n      targetBlock.selected = true;\n\n      BlockSelection.clearCache();\n\n      return;\n    }\n\n    if (targetBlock === this.firstSelectedBlock) {\n      relatedBlock.selected = false;\n      targetBlock.selected = false;\n\n      BlockSelection.clearCache();\n\n      return;\n    }\n\n    this.Editor.InlineToolbar.close();\n\n    this.toggleBlocksSelectedState(relatedBlock, targetBlock);\n    this.lastSelectedBlock = targetBlock;\n  };\n\n  /**\n   * Change blocks selection state between passed two blocks.\n   *\n   * @param {Block} firstBlock - first block in range\n   * @param {Block} lastBlock - last block in range\n   */\n  private toggleBlocksSelectedState(firstBlock: Block, lastBlock: Block): void {\n    const { BlockManager, BlockSelection } = this.Editor;\n    const fIndex = BlockManager.blocks.indexOf(firstBlock);\n    const lIndex = BlockManager.blocks.indexOf(lastBlock);\n\n    /**\n     * If first and last block have the different selection state\n     * it means we should't toggle selection of the first selected block.\n     * In the other case we shouldn't toggle the last selected block.\n     */\n    const shouldntSelectFirstBlock = firstBlock.selected !== lastBlock.selected;\n\n    for (let i = Math.min(fIndex, lIndex); i <= Math.max(fIndex, lIndex); i++) {\n      const block = BlockManager.blocks[i];\n\n      if (\n        block !== this.firstSelectedBlock &&\n        block !== (shouldntSelectFirstBlock ? firstBlock : lastBlock)\n      ) {\n        BlockManager.blocks[i].selected = !BlockManager.blocks[i].selected;\n\n        BlockSelection.clearCache();\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/modules/dragNDrop.ts",
    "content": "import SelectionUtils from '../selection';\n\nimport Module from '../__module';\n/**\n *\n */\nexport default class DragNDrop extends Module {\n  /**\n   * If drag has been started at editor, we save it\n   *\n   * @type {boolean}\n   * @private\n   */\n  private isStartedAtEditor = false;\n\n  /**\n   * Toggle read-only state\n   *\n   * if state is true:\n   *  - disable all drag-n-drop event handlers\n   *\n   * if state is false:\n   *  - restore drag-n-drop event handlers\n   *\n   * @param {boolean} readOnlyEnabled - \"read only\" state\n   */\n  public toggleReadOnly(readOnlyEnabled: boolean): void {\n    if (readOnlyEnabled) {\n      this.disableModuleBindings();\n    } else {\n      this.enableModuleBindings();\n    }\n  }\n\n  /**\n   * Add drag events listeners to editor zone\n   */\n  private enableModuleBindings(): void {\n    const { UI } = this.Editor;\n\n    this.readOnlyMutableListeners.on(UI.nodes.holder, 'drop', async (dropEvent: DragEvent) => {\n      await this.processDrop(dropEvent);\n    }, true);\n\n    this.readOnlyMutableListeners.on(UI.nodes.holder, 'dragstart', () => {\n      this.processDragStart();\n    });\n\n    /**\n     * Prevent default browser behavior to allow drop on non-contenteditable elements\n     */\n    this.readOnlyMutableListeners.on(UI.nodes.holder, 'dragover', (dragEvent: DragEvent) => {\n      this.processDragOver(dragEvent);\n    }, true);\n  }\n\n  /**\n   * Unbind drag-n-drop event handlers\n   */\n  private disableModuleBindings(): void {\n    this.readOnlyMutableListeners.clearAll();\n  }\n\n  /**\n   * Handle drop event\n   *\n   * @param {DragEvent} dropEvent - drop event\n   */\n  private async processDrop(dropEvent: DragEvent): Promise<void> {\n    const {\n      BlockManager,\n      Paste,\n      Caret,\n    } = this.Editor;\n\n    dropEvent.preventDefault();\n\n    BlockManager.blocks.forEach((block) => {\n      block.dropTarget = false;\n    });\n\n    if (SelectionUtils.isAtEditor && !SelectionUtils.isCollapsed && this.isStartedAtEditor) {\n      document.execCommand('delete');\n    }\n\n    this.isStartedAtEditor = false;\n\n    /**\n     * Try to set current block by drop target.\n     * If drop target is not part of the Block, set last Block as current.\n     */\n    const targetBlock = BlockManager.setCurrentBlockByChildNode(dropEvent.target as Node);\n\n    if (targetBlock) {\n      this.Editor.Caret.setToBlock(targetBlock, Caret.positions.END);\n    } else {\n      const lastBlock = BlockManager.setCurrentBlockByChildNode(BlockManager.lastBlock.holder);\n\n      this.Editor.Caret.setToBlock(lastBlock, Caret.positions.END);\n    }\n\n    await Paste.processDataTransfer(dropEvent.dataTransfer, true);\n  }\n\n  /**\n   * Handle drag start event\n   */\n  private processDragStart(): void {\n    if (SelectionUtils.isAtEditor && !SelectionUtils.isCollapsed) {\n      this.isStartedAtEditor = true;\n    }\n\n    this.Editor.InlineToolbar.close();\n  }\n\n  /**\n   * @param {DragEvent} dragEvent - drag event\n   */\n  private processDragOver(dragEvent: DragEvent): void {\n    dragEvent.preventDefault();\n  }\n}\n"
  },
  {
    "path": "src/components/modules/index.ts",
    "content": "/** ./api */\nimport BlocksAPI from './api/blocks';\nimport CaretAPI from './api/caret';\nimport EventsAPI from './api/events';\nimport I18nAPI from './api/i18n';\nimport API from './api/index';\nimport InlineToolbarAPI from './api/inlineToolbar';\nimport ListenersAPI from './api/listeners';\nimport NotifierAPI from './api/notifier';\nimport ReadOnlyAPI from './api/readonly';\nimport SanitizerAPI from './api/sanitizer';\nimport SaverAPI from './api/saver';\nimport SelectionAPI from './api/selection';\nimport ToolsAPI from './api/tools';\nimport StylesAPI from './api/styles';\nimport ToolbarAPI from './api/toolbar';\nimport TooltipAPI from './api/tooltip';\nimport UiAPI from './api/ui';\n\n/** ./toolbar */\nimport BlockSettings from './toolbar/blockSettings';\nimport Toolbar from './toolbar/index';\nimport InlineToolbar from './toolbar/inline';\n\n/** . */\nimport BlockEvents from './blockEvents';\nimport BlockManager from './blockManager';\nimport BlockSelection from './blockSelection';\nimport Caret from './caret';\nimport CrossBlockSelection from './crossBlockSelection';\nimport DragNDrop from './dragNDrop';\nimport ModificationsObserver from './modificationsObserver';\nimport Paste from './paste';\nimport ReadOnly from './readonly';\nimport RectangleSelection from './rectangleSelection';\nimport Renderer from './renderer';\nimport Saver from './saver';\nimport Tools from './tools';\nimport UI from './ui';\n\nexport default {\n  // API Modules\n  BlocksAPI,\n  CaretAPI,\n  EventsAPI,\n  I18nAPI,\n  API,\n  InlineToolbarAPI,\n  ListenersAPI,\n  NotifierAPI,\n  ReadOnlyAPI,\n  SanitizerAPI,\n  SaverAPI,\n  SelectionAPI,\n  ToolsAPI,\n  StylesAPI,\n  ToolbarAPI,\n  TooltipAPI,\n  UiAPI,\n\n  // Toolbar Modules\n  BlockSettings,\n  Toolbar,\n  InlineToolbar,\n\n  // Modules\n  BlockEvents,\n  BlockManager,\n  BlockSelection,\n  Caret,\n  CrossBlockSelection,\n  DragNDrop,\n  ModificationsObserver,\n  Paste,\n  ReadOnly,\n  RectangleSelection,\n  Renderer,\n  Saver,\n  Tools,\n  UI,\n};\n"
  },
  {
    "path": "src/components/modules/modificationsObserver.ts",
    "content": "import type { BlockId } from '../../../types';\nimport type { BlockMutationEvent, BlockMutationType } from '../../../types/events/block';\nimport type { ModuleConfig } from '../../types-internal/module-config';\nimport Module from '../__module';\nimport { modificationsObserverBatchTimeout } from '../constants';\nimport { BlockChanged, FakeCursorAboutToBeToggled, FakeCursorHaveBeenSet, RedactorDomChanged } from '../events';\nimport * as _ from '../utils';\n\n/**\n * We use map of block mutations to filter only unique events\n */\ntype UniqueBlockMutationKey = `block:${BlockId}:event:${BlockMutationType}`;\n\n/**\n * Single entry point for Block mutation events\n */\nexport default class ModificationsObserver extends Module {\n  /**\n   * Flag shows onChange event is disabled\n   */\n  private disabled = false;\n\n  /**\n   * Blocks wrapper mutation observer instance\n   */\n  private readonly mutationObserver: MutationObserver;\n\n  /**\n   * Timeout used to batched several events in a single onChange call\n   */\n  private batchingTimeout: null | ReturnType<typeof setTimeout> = null;\n\n  /**\n   * Array of onChange events used to batch them\n   *\n   * Map is used to filter duplicated events related to the same block\n   */\n  private batchingOnChangeQueue = new Map<UniqueBlockMutationKey, BlockMutationEvent>();\n\n  /**\n   * Fired onChange events will be batched by this time\n   */\n  private readonly batchTime = modificationsObserverBatchTimeout;\n\n  /**\n   * Prepare the module\n   *\n   * @param options - options used by the modification observer module\n   * @param options.config - Editor configuration object\n   * @param options.eventsDispatcher - common Editor event bus\n   */\n  constructor({ config, eventsDispatcher }: ModuleConfig) {\n    super({\n      config,\n      eventsDispatcher,\n    });\n\n    this.mutationObserver = new MutationObserver((mutations) => {\n      this.redactorChanged(mutations);\n    });\n\n    this.eventsDispatcher.on(BlockChanged, (payload) => {\n      this.particularBlockChanged(payload.event);\n    });\n\n    /**\n     * Mutex for fake cursor setting/removing operation\n     */\n    this.eventsDispatcher.on(FakeCursorAboutToBeToggled, () => {\n      this.disable();\n    });\n\n    this.eventsDispatcher.on(FakeCursorHaveBeenSet, () => {\n      this.enable();\n    });\n  }\n\n  /**\n   * Enables onChange event\n   */\n  public enable(): void {\n    this.mutationObserver.observe(\n      this.Editor.UI.nodes.redactor,\n      {\n        childList: true,\n        subtree: true,\n        characterData: true,\n        attributes: true,\n      }\n    );\n    this.disabled = false;\n  }\n\n  /**\n   * Disables onChange event\n   */\n  public disable(): void {\n    this.mutationObserver.disconnect();\n    this.disabled = true;\n  }\n\n  /**\n   * Call onChange event passed to Editor.js configuration\n   *\n   * @param event - some of our custom change events\n   */\n  private particularBlockChanged(event: BlockMutationEvent): void {\n    if (this.disabled || !_.isFunction(this.config.onChange)) {\n      return;\n    }\n\n    this.batchingOnChangeQueue.set(`block:${event.detail.target.id}:event:${event.type as BlockMutationType}`, event);\n\n    if (this.batchingTimeout) {\n      clearTimeout(this.batchingTimeout);\n    }\n\n    this.batchingTimeout = setTimeout(() => {\n      let eventsToEmit;\n\n      /**\n       * Ih we have only 1 event in a queue, unwrap it\n       */\n      if (this.batchingOnChangeQueue.size === 1) {\n        eventsToEmit = this.batchingOnChangeQueue.values().next().value;\n      } else {\n        eventsToEmit = Array.from(this.batchingOnChangeQueue.values());\n      }\n\n      if (this.config.onChange) {\n        this.config.onChange(this.Editor.API.methods, eventsToEmit);\n      }\n\n      this.batchingOnChangeQueue.clear();\n    }, this.batchTime);\n  }\n\n  /**\n   * Fired on every blocks wrapper dom change\n   *\n   * @param mutations - mutations happened\n   */\n  private redactorChanged(mutations: MutationRecord[]): void {\n    this.eventsDispatcher.emit(RedactorDomChanged, {\n      mutations,\n    });\n  }\n}\n"
  },
  {
    "path": "src/components/modules/paste.ts",
    "content": "import Module from '../__module';\nimport $ from '../dom';\nimport * as _ from '../utils';\nimport type {\n  BlockAPI,\n  PasteEvent,\n  PasteEventDetail,\n  SanitizerConfig,\n  SanitizerRule\n} from '../../../types';\nimport type Block from '../block';\nimport type { SavedData } from '../../../types/data-formats';\nimport { clean, sanitizeBlocks } from '../utils/sanitizer';\nimport type BlockToolAdapter from '../tools/block';\n\n/**\n * Tag substitute object.\n */\ninterface TagSubstitute {\n  /**\n   * Name of related Tool\n   *\n   */\n  tool: BlockToolAdapter;\n\n  /**\n   * If a Tool specifies just a tag name, all the attributes will be sanitized.\n   * But Tool can explicitly specify sanitizer configuration for supported tags\n   */\n  sanitizationConfig?: SanitizerRule;\n}\n\n/**\n * Pattern substitute object.\n */\ninterface PatternSubstitute {\n  /**\n   * Pattern`s key\n   */\n  key: string;\n\n  /**\n   * Pattern regexp\n   */\n  pattern: RegExp;\n\n  /**\n   * Name of related Tool\n   */\n  tool: BlockToolAdapter;\n}\n\n/**\n * Files` types substitutions object.\n */\ninterface FilesSubstitution {\n  /**\n   * Array of file extensions Tool can handle\n   *\n   * @type {string[]}\n   */\n  extensions: string[];\n\n  /**\n   * Array of MIME types Tool can handle\n   *\n   * @type {string[]}\n   */\n  mimeTypes: string[];\n}\n\n/**\n * Processed paste data object.\n *\n * @interface PasteData\n */\ninterface PasteData {\n  /**\n   * Name of related Tool\n   *\n   * @type {string}\n   */\n  tool: string;\n\n  /**\n   * Pasted data. Processed and wrapped to HTML element\n   *\n   * @type {HTMLElement}\n   */\n  content: HTMLElement;\n\n  /**\n   * Pasted data\n   */\n  event: PasteEvent;\n\n  /**\n   * True if content should be inserted as new Block\n   *\n   * @type {boolean}\n   */\n  isBlock: boolean;\n}\n\n/**\n * @class Paste\n * @classdesc Contains methods to handle paste on editor\n * @module Paste\n * @version 2.0.0\n */\nexport default class Paste extends Module {\n  /** If string`s length is greater than this number we don't check paste patterns */\n  public static readonly PATTERN_PROCESSING_MAX_LENGTH = 450;\n\n  /** Custom EditorJS mime-type to handle in-editor copy/paste actions */\n  public readonly MIME_TYPE = 'application/x-editor-js';\n\n  /**\n   * Tags` substitutions parameters\n   */\n  private toolsTags: { [tag: string]: TagSubstitute } = {};\n\n  /**\n   * Store tags to substitute by tool name\n   */\n  private tagsByTool: { [tools: string]: string[] } = {};\n\n  /** Patterns` substitutions parameters */\n  private toolsPatterns: PatternSubstitute[] = [];\n\n  /** Files` substitutions parameters */\n  private toolsFiles: {\n    [tool: string]: FilesSubstitution;\n  } = {};\n\n  /**\n   * List of tools which do not need a paste handling\n   */\n  private exceptionList: string[] = [];\n\n  /**\n   * Set onPaste callback and collect tools` paste configurations\n   */\n  public async prepare(): Promise<void> {\n    this.processTools();\n  }\n\n  /**\n   * Set read-only state\n   *\n   * @param {boolean} readOnlyEnabled - read only flag value\n   */\n  public toggleReadOnly(readOnlyEnabled: boolean): void {\n    if (!readOnlyEnabled) {\n      this.setCallback();\n    } else {\n      this.unsetCallback();\n    }\n  }\n\n  /**\n   * Handle pasted or dropped data transfer object\n   *\n   * @param {DataTransfer} dataTransfer - pasted or dropped data transfer object\n   * @param {boolean} isDragNDrop - true if data transfer comes from drag'n'drop events\n   */\n  public async processDataTransfer(dataTransfer: DataTransfer, isDragNDrop = false): Promise<void> {\n    const { Tools } = this.Editor;\n    const types = dataTransfer.types;\n\n    /**\n     * In Microsoft Edge types is DOMStringList. So 'contains' is used to check if 'Files' type included\n     */\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    const includesFiles = types.includes ? types.includes('Files') : (types as any).contains('Files');\n\n    if (includesFiles && !_.isEmpty(this.toolsFiles)) {\n      await this.processFiles(dataTransfer.files);\n\n      return;\n    }\n\n    const editorJSData = dataTransfer.getData(this.MIME_TYPE);\n    const plainData = dataTransfer.getData('text/plain');\n    let htmlData = dataTransfer.getData('text/html');\n\n    /**\n     * If EditorJS json is passed, insert it\n     */\n    if (editorJSData) {\n      try {\n        this.insertEditorJSData(JSON.parse(editorJSData));\n\n        return;\n      } catch (e) { } // Do nothing and continue execution as usual if error appears\n    }\n\n    /**\n     *  If text was drag'n'dropped, wrap content with P tag to insert it as the new Block\n     */\n    if (isDragNDrop && plainData.trim() && htmlData.trim()) {\n      htmlData = '<p>' + (htmlData.trim() ? htmlData : plainData) + '</p>';\n    }\n\n    /** Add all tags that can be substituted to sanitizer configuration */\n    const toolsTags = Object.keys(this.toolsTags).reduce((result, tag) => {\n      /**\n       * If Tool explicitly specifies sanitizer configuration for the tag, use it.\n       * Otherwise, remove all attributes\n       */\n      result[tag.toLowerCase()] = this.toolsTags[tag].sanitizationConfig ?? {};\n\n      return result;\n    }, {});\n\n    const customConfig = Object.assign({}, toolsTags, Tools.getAllInlineToolsSanitizeConfig(), { br: {} });\n    const cleanData = clean(htmlData, customConfig);\n\n    /** If there is no HTML or HTML string is equal to plain one, process it as plain text */\n    if (!cleanData.trim() || cleanData.trim() === plainData || !$.isHTMLString(cleanData)) {\n      await this.processText(plainData);\n    } else {\n      await this.processText(cleanData, true);\n    }\n  }\n\n  /**\n   * Process pasted text and divide them into Blocks\n   *\n   * @param {string} data - text to process. Can be HTML or plain.\n   * @param {boolean} isHTML - if passed string is HTML, this parameter should be true\n   */\n  public async processText(data: string, isHTML = false): Promise<void> {\n    const { Caret, BlockManager } = this.Editor;\n    const dataToInsert = isHTML ? this.processHTML(data) : this.processPlain(data);\n\n    if (!dataToInsert.length) {\n      return;\n    }\n\n    if (dataToInsert.length === 1) {\n      if (!dataToInsert[0].isBlock) {\n        this.processInlinePaste(dataToInsert.pop());\n      } else {\n        this.processSingleBlock(dataToInsert.pop());\n      }\n\n      return;\n    }\n\n    const isCurrentBlockDefault = BlockManager.currentBlock && BlockManager.currentBlock.tool.isDefault;\n    const needToReplaceCurrentBlock = isCurrentBlockDefault && BlockManager.currentBlock.isEmpty;\n\n    dataToInsert.map(\n      async (content, i) => this.insertBlock(content, i === 0 && needToReplaceCurrentBlock)\n    );\n\n    if (BlockManager.currentBlock) {\n      Caret.setToBlock(BlockManager.currentBlock, Caret.positions.END);\n    }\n  }\n\n  /**\n   * Set onPaste callback handler\n   */\n  private setCallback(): void {\n    this.listeners.on(this.Editor.UI.nodes.holder, 'paste', this.handlePasteEvent);\n  }\n\n  /**\n   * Unset onPaste callback handler\n   */\n  private unsetCallback(): void {\n    this.listeners.off(this.Editor.UI.nodes.holder, 'paste', this.handlePasteEvent);\n  }\n\n  /**\n   * Get and process tool`s paste configs\n   */\n  private processTools(): void {\n    const tools = this.Editor.Tools.blockTools;\n\n    Array\n      .from(tools.values())\n      .forEach(this.processTool);\n  }\n\n  /**\n   * Process paste config for each tool\n   *\n   * @param tool - BlockTool object\n   */\n  private processTool = (tool: BlockToolAdapter): void => {\n    try {\n      const toolInstance = tool.create({}, {} as BlockAPI, false);\n\n      if (tool.pasteConfig === false) {\n        this.exceptionList.push(tool.name);\n\n        return;\n      }\n\n      if (!_.isFunction(toolInstance.onPaste)) {\n        return;\n      }\n\n      this.getTagsConfig(tool);\n      this.getFilesConfig(tool);\n      this.getPatternsConfig(tool);\n    } catch (e) {\n      _.log(\n        `Paste handling for «${tool.name}» Tool hasn't been set up because of the error`,\n        'warn',\n        e\n      );\n    }\n  };\n\n  /**\n   * Get tags name list from either tag name or sanitization config.\n   *\n   * @param {string | object} tagOrSanitizeConfig - tag name or sanitize config object.\n   * @returns {string[]} array of tags.\n   */\n  private collectTagNames(tagOrSanitizeConfig: string | SanitizerConfig): string[] {\n    /**\n     * If string, then it is a tag name.\n     */\n    if (_.isString(tagOrSanitizeConfig)) {\n      return [ tagOrSanitizeConfig ];\n    }\n    /**\n     * If object, then its keys are tags.\n     */\n    if (_.isObject(tagOrSanitizeConfig)) {\n      return Object.keys(tagOrSanitizeConfig);\n    }\n\n    /** Return empty tag list */\n    return [];\n  }\n\n  /**\n   * Get tags to substitute by Tool\n   *\n   * @param tool - BlockTool object\n   */\n  private getTagsConfig(tool: BlockToolAdapter): void {\n    if (tool.pasteConfig === false) {\n      return;\n    }\n\n    const tagsOrSanitizeConfigs = tool.pasteConfig.tags || [];\n    const toolTags = [];\n\n    tagsOrSanitizeConfigs.forEach((tagOrSanitizeConfig) => {\n      const tags = this.collectTagNames(tagOrSanitizeConfig);\n\n      /**\n       * Add tags to toolTags array\n       */\n      toolTags.push(...tags);\n      tags.forEach((tag) => {\n        if (Object.prototype.hasOwnProperty.call(this.toolsTags, tag)) {\n          _.log(\n            `Paste handler for «${tool.name}» Tool on «${tag}» tag is skipped ` +\n            `because it is already used by «${this.toolsTags[tag].tool.name}» Tool.`,\n            'warn'\n          );\n\n          return;\n        }\n        /**\n         * Get sanitize config for tag.\n         */\n        const sanitizationConfig = _.isObject(tagOrSanitizeConfig) ? tagOrSanitizeConfig[tag] : null;\n\n        this.toolsTags[tag.toUpperCase()] = {\n          tool,\n          sanitizationConfig,\n        };\n      });\n    });\n\n    this.tagsByTool[tool.name] = toolTags.map((t) => t.toUpperCase());\n  }\n\n  /**\n   * Get files` types and extensions to substitute by Tool\n   *\n   * @param tool - BlockTool object\n   */\n  private getFilesConfig(tool: BlockToolAdapter): void {\n    if (tool.pasteConfig === false) {\n      return;\n    }\n\n    const { files = {} } = tool.pasteConfig;\n    let { extensions, mimeTypes } = files;\n\n    if (!extensions && !mimeTypes) {\n      return;\n    }\n\n    if (extensions && !Array.isArray(extensions)) {\n      _.log(`«extensions» property of the onDrop config for «${tool.name}» Tool should be an array`);\n      extensions = [];\n    }\n\n    if (mimeTypes && !Array.isArray(mimeTypes)) {\n      _.log(`«mimeTypes» property of the onDrop config for «${tool.name}» Tool should be an array`);\n      mimeTypes = [];\n    }\n\n    if (mimeTypes) {\n      mimeTypes = mimeTypes.filter((type) => {\n        if (!_.isValidMimeType(type)) {\n          _.log(`MIME type value «${type}» for the «${tool.name}» Tool is not a valid MIME type`, 'warn');\n\n          return false;\n        }\n\n        return true;\n      });\n    }\n\n    this.toolsFiles[tool.name] = {\n      extensions: extensions || [],\n      mimeTypes: mimeTypes || [],\n    };\n  }\n\n  /**\n   * Get RegExp patterns to substitute by Tool\n   *\n   * @param tool - BlockTool object\n   */\n  private getPatternsConfig(tool: BlockToolAdapter): void {\n    if (\n      tool.pasteConfig === false ||\n      !tool.pasteConfig.patterns ||\n      _.isEmpty(tool.pasteConfig.patterns)\n    ) {\n      return;\n    }\n\n    Object.entries(tool.pasteConfig.patterns).forEach(([key, pattern]: [string, RegExp]) => {\n      /** Still need to validate pattern as it provided by user */\n      if (!(pattern instanceof RegExp)) {\n        _.log(\n          `Pattern ${pattern} for «${tool.name}» Tool is skipped because it should be a Regexp instance.`,\n          'warn'\n        );\n      }\n\n      this.toolsPatterns.push({\n        key,\n        pattern,\n        tool,\n      });\n    });\n  }\n\n  /**\n   * Check if browser behavior suits better\n   *\n   * @param {EventTarget} element - element where content has been pasted\n   * @returns {boolean}\n   */\n  private isNativeBehaviour(element: EventTarget): boolean {\n    return $.isNativeInput(element);\n  }\n\n  /**\n   * Check if Editor should process pasted data and pass data transfer object to handler\n   *\n   * @param {ClipboardEvent} event - clipboard event\n   */\n  private handlePasteEvent = async (event: ClipboardEvent): Promise<void> => {\n    const { BlockManager, Toolbar } = this.Editor;\n\n    /**\n     * When someone pasting into a block, its more stable to set current block by event target, instead of relying on current block set before\n     */\n    const currentBlock = BlockManager.setCurrentBlockByChildNode(event.target as HTMLElement);\n\n    /** If target is native input or is not Block, use browser behaviour */\n    if (\n      !currentBlock || (this.isNativeBehaviour(event.target) && !event.clipboardData.types.includes('Files'))\n    ) {\n      return;\n    }\n\n    /**\n     * If Tools is in list of errors, skip processing of paste event\n     */\n    if (currentBlock && this.exceptionList.includes(currentBlock.name)) {\n      return;\n    }\n\n    event.preventDefault();\n    this.processDataTransfer(event.clipboardData);\n\n    Toolbar.close();\n  };\n\n  /**\n   * Get files from data transfer object and insert related Tools\n   *\n   * @param {FileList} items - pasted or dropped items\n   */\n  private async processFiles(items: FileList): Promise<void> {\n    const { BlockManager } = this.Editor;\n\n    let dataToInsert: { type: string; event: PasteEvent }[];\n\n    dataToInsert = await Promise.all(\n      Array\n        .from(items)\n        .map((item) => this.processFile(item))\n    );\n    dataToInsert = dataToInsert.filter((data) => !!data);\n\n    const isCurrentBlockDefault = BlockManager.currentBlock.tool.isDefault;\n    const needToReplaceCurrentBlock = isCurrentBlockDefault && BlockManager.currentBlock.isEmpty;\n\n    dataToInsert.forEach(\n      (data, i) => {\n        BlockManager.paste(data.type, data.event, i === 0 && needToReplaceCurrentBlock);\n      }\n    );\n  }\n\n  /**\n   * Get information about file and find Tool to handle it\n   *\n   * @param {File} file - file to process\n   */\n  private async processFile(file: File): Promise<{ event: PasteEvent; type: string }> {\n    const extension = _.getFileExtension(file);\n\n    const foundConfig = Object\n      .entries(this.toolsFiles)\n      // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars\n      .find(([toolName, { mimeTypes, extensions } ]) => {\n        const [fileType, fileSubtype] = file.type.split('/');\n\n        const foundExt = extensions.find((ext) => ext.toLowerCase() === extension.toLowerCase());\n        const foundMimeType = mimeTypes.find((mime) => {\n          const [type, subtype] = mime.split('/');\n\n          return type === fileType && (subtype === fileSubtype || subtype === '*');\n        });\n\n        return !!foundExt || !!foundMimeType;\n      });\n\n    if (!foundConfig) {\n      return;\n    }\n\n    const [ tool ] = foundConfig;\n    const pasteEvent = this.composePasteEvent('file', {\n      file,\n    });\n\n    return {\n      event: pasteEvent,\n      type: tool,\n    };\n  }\n\n  /**\n   * Split HTML string to blocks and return it as array of Block data\n   *\n   * @param {string} innerHTML - html string to process\n   * @returns {PasteData[]}\n   */\n  private processHTML(innerHTML: string): PasteData[] {\n    const { Tools } = this.Editor;\n\n    /**\n     * @todo Research, do we really need to always wrap innerHTML to a div:\n     *  - <img> tag could be processed separately, but for now it becomes div-wrapped\n     *    and then .getNodes() returns strange: [document-fragment, img]\n     *    (description of the method says that it should should return only block tags or fragments,\n     *     but there are inline-block element along with redundant empty fragment)\n     *  - probably this is a reason of bugs with unexpected new block creation instead of inline pasting:\n     *      - https://github.com/codex-team/editor.js/issues/1427\n     *      - https://github.com/codex-team/editor.js/issues/1244\n     *      - https://github.com/codex-team/editor.js/issues/740\n     */\n    const wrapper = $.make('DIV');\n\n    wrapper.innerHTML = innerHTML;\n\n    const nodes = this.getNodes(wrapper);\n\n    return nodes\n      .map((node) => {\n        let content, tool = Tools.defaultTool, isBlock = false;\n\n        switch (node.nodeType) {\n          /** If node is a document fragment, use temp wrapper to get innerHTML */\n          case Node.DOCUMENT_FRAGMENT_NODE:\n            content = $.make('div');\n            content.appendChild(node);\n            break;\n\n          /** If node is an element, then there might be a substitution */\n          case Node.ELEMENT_NODE:\n            content = node as HTMLElement;\n            isBlock = true;\n\n            if (this.toolsTags[content.tagName]) {\n              tool = this.toolsTags[content.tagName].tool;\n            }\n            break;\n        }\n\n        /**\n         * Returns empty array if there is no paste config\n         */\n        const { tags: tagsOrSanitizeConfigs } = tool.pasteConfig || { tags: [] };\n\n        /**\n         * Reduce the tags or sanitize configs to a single array of sanitize config.\n         * For example:\n         * If sanitize config is\n         * [ 'tbody',\n         *   {\n         *     table: {\n         *       width: true,\n         *       height: true,\n         *     },\n         *   },\n         *   {\n         *      td: {\n         *        colspan: true,\n         *        rowspan: true,\n         *      },\n         *      tr: {  // <-- the second tag\n         *        height: true,\n         *      },\n         *   },\n         * ]\n         * then sanitize config will be\n         * [\n         *  'table':{},\n         *  'tbody':{width: true, height: true}\n         *  'td':{colspan: true, rowspan: true},\n         *  'tr':{height: true}\n         * ]\n         */\n        const toolTags = tagsOrSanitizeConfigs.reduce((result, tagOrSanitizeConfig) => {\n          const tags = this.collectTagNames(tagOrSanitizeConfig);\n\n          tags.forEach((tag) => {\n            const sanitizationConfig = _.isObject(tagOrSanitizeConfig) ? tagOrSanitizeConfig[tag] : null;\n\n            result[tag.toLowerCase()] = sanitizationConfig || {};\n          });\n\n          return result;\n        }, {});\n\n        const customConfig = Object.assign({}, toolTags, tool.baseSanitizeConfig);\n\n        /**\n         * A workaround for the HTMLJanitor bug with Tables (incorrect sanitizing of table.innerHTML)\n         * https://github.com/guardian/html-janitor/issues/3\n         */\n        if (content.tagName.toLowerCase() === 'table') {\n          const cleanTableHTML = clean(content.outerHTML, customConfig);\n          const tmpWrapper = $.make('div', undefined, {\n            innerHTML: cleanTableHTML,\n          });\n\n          content = tmpWrapper.firstChild;\n        } else {\n          content.innerHTML = clean(content.innerHTML, customConfig);\n        }\n\n        const event = this.composePasteEvent('tag', {\n          data: content,\n        });\n\n        return {\n          content,\n          isBlock,\n          tool: tool.name,\n          event,\n        };\n      })\n      .filter((data) => {\n        const isEmpty = $.isEmpty(data.content);\n        const isSingleTag = $.isSingleTag(data.content);\n\n        return !isEmpty || isSingleTag;\n      });\n  }\n\n  /**\n   * Split plain text by new line symbols and return it as array of Block data\n   *\n   * @param {string} plain - string to process\n   * @returns {PasteData[]}\n   */\n  private processPlain(plain: string): PasteData[] {\n    const { defaultBlock } = this.config as { defaultBlock: string };\n\n    if (!plain) {\n      return [];\n    }\n\n    const tool = defaultBlock;\n\n    return plain\n      .split(/\\r?\\n/)\n      .filter((text) => text.trim())\n      .map((text) => {\n        const content = $.make('div');\n\n        content.textContent = text;\n\n        const event = this.composePasteEvent('tag', {\n          data: content,\n        });\n\n        return {\n          content,\n          tool,\n          isBlock: false,\n          event,\n        };\n      });\n  }\n\n  /**\n   * Process paste of single Block tool content\n   *\n   * @param {PasteData} dataToInsert - data of Block to insert\n   */\n  private async processSingleBlock(dataToInsert: PasteData): Promise<void> {\n    const { Caret, BlockManager } = this.Editor;\n    const { currentBlock } = BlockManager;\n\n    /**\n     * If pasted tool isn`t equal current Block or if pasted content contains block elements, insert it as new Block\n     */\n    if (\n      !currentBlock ||\n      dataToInsert.tool !== currentBlock.name ||\n      !$.containsOnlyInlineElements(dataToInsert.content.innerHTML)\n    ) {\n      this.insertBlock(dataToInsert, currentBlock?.tool.isDefault && currentBlock.isEmpty);\n\n      return;\n    }\n\n    Caret.insertContentAtCaretPosition(dataToInsert.content.innerHTML);\n  }\n\n  /**\n   * Process paste to single Block:\n   * 1. Find patterns` matches\n   * 2. Insert new block if it is not the same type as current one\n   * 3. Just insert text if there is no substitutions\n   *\n   * @param {PasteData} dataToInsert - data of Block to insert\n   */\n  private async processInlinePaste(dataToInsert: PasteData): Promise<void> {\n    const { BlockManager, Caret } = this.Editor;\n    const { content } = dataToInsert;\n\n    const currentBlockIsDefault = BlockManager.currentBlock && BlockManager.currentBlock.tool.isDefault;\n\n    if (currentBlockIsDefault && content.textContent.length < Paste.PATTERN_PROCESSING_MAX_LENGTH) {\n      const blockData = await this.processPattern(content.textContent);\n\n      if (blockData) {\n        const needToReplaceCurrentBlock = BlockManager.currentBlock &&\n          BlockManager.currentBlock.tool.isDefault &&\n          BlockManager.currentBlock.isEmpty;\n\n        const insertedBlock = BlockManager.paste(blockData.tool, blockData.event, needToReplaceCurrentBlock);\n\n        Caret.setToBlock(insertedBlock, Caret.positions.END);\n\n        return;\n      }\n    }\n\n    /** If there is no pattern substitute - insert string as it is */\n    if (BlockManager.currentBlock && BlockManager.currentBlock.currentInput) {\n      const currentToolSanitizeConfig = BlockManager.currentBlock.tool.baseSanitizeConfig;\n\n      document.execCommand(\n        'insertHTML',\n        false,\n        clean(content.innerHTML, currentToolSanitizeConfig)\n      );\n    } else {\n      this.insertBlock(dataToInsert);\n    }\n  }\n\n  /**\n   * Get patterns` matches\n   *\n   * @param {string} text - text to process\n   * @returns {Promise<{event: PasteEvent, tool: string}>}\n   */\n  private async processPattern(text: string): Promise<{ event: PasteEvent; tool: string }> {\n    const pattern = this.toolsPatterns.find((substitute) => {\n      const execResult = substitute.pattern.exec(text);\n\n      if (!execResult) {\n        return false;\n      }\n\n      return text === execResult.shift();\n    });\n\n    if (!pattern) {\n      return;\n    }\n\n    const event = this.composePasteEvent('pattern', {\n      key: pattern.key,\n      data: text,\n    });\n\n    return {\n      event,\n      tool: pattern.tool.name,\n    };\n  }\n\n  /**\n   * Insert pasted Block content to Editor\n   *\n   * @param {PasteData} data - data to insert\n   * @param {boolean} canReplaceCurrentBlock - if true and is current Block is empty, will replace current Block\n   * @returns {void}\n   */\n  private insertBlock(data: PasteData, canReplaceCurrentBlock = false): void {\n    const { BlockManager, Caret } = this.Editor;\n    const { currentBlock } = BlockManager;\n    let block: Block;\n\n    if (canReplaceCurrentBlock && currentBlock && currentBlock.isEmpty) {\n      block = BlockManager.paste(data.tool, data.event, true);\n      Caret.setToBlock(block, Caret.positions.END);\n\n      return;\n    }\n\n    block = BlockManager.paste(data.tool, data.event);\n\n    Caret.setToBlock(block, Caret.positions.END);\n  }\n\n  /**\n   * Insert data passed as application/x-editor-js JSON\n   *\n   * @param {Array} blocks — Blocks' data to insert\n   * @returns {void}\n   */\n  private insertEditorJSData(blocks: Pick<SavedData, 'id' | 'data' | 'tool'>[]): void {\n    const { BlockManager, Caret, Tools } = this.Editor;\n    const sanitizedBlocks = sanitizeBlocks(blocks, (name) =>\n      Tools.blockTools.get(name).sanitizeConfig\n    );\n\n    sanitizedBlocks.forEach(({ tool, data }, i) => {\n      let needToReplaceCurrentBlock = false;\n\n      if (i === 0) {\n        const isCurrentBlockDefault = BlockManager.currentBlock && BlockManager.currentBlock.tool.isDefault;\n\n        needToReplaceCurrentBlock = isCurrentBlockDefault && BlockManager.currentBlock.isEmpty;\n      }\n\n      const block = BlockManager.insert({\n        tool,\n        data,\n        replace: needToReplaceCurrentBlock,\n      });\n\n      Caret.setToBlock(block, Caret.positions.END);\n    });\n  }\n\n  /**\n   * Fetch nodes from Element node\n   *\n   * @param {Node} node - current node\n   * @param {Node[]} nodes - processed nodes\n   * @param {Node} destNode - destination node\n   */\n  private processElementNode(node: Node, nodes: Node[], destNode: Node): Node[] | void {\n    const tags = Object.keys(this.toolsTags);\n\n    const element = node as HTMLElement;\n\n    const { tool } = this.toolsTags[element.tagName] || {};\n    const toolTags = this.tagsByTool[tool?.name] || [];\n\n    const isSubstitutable = tags.includes(element.tagName);\n    const isBlockElement = $.blockElements.includes(element.tagName.toLowerCase());\n    const containsAnotherToolTags = Array\n      .from(element.children)\n      .some(\n        ({ tagName }) => tags.includes(tagName) && !toolTags.includes(tagName)\n      );\n\n    const containsBlockElements = Array.from(element.children).some(\n      ({ tagName }) => $.blockElements.includes(tagName.toLowerCase())\n    );\n\n    /** Append inline elements to previous fragment */\n    if (!isBlockElement && !isSubstitutable && !containsAnotherToolTags) {\n      destNode.appendChild(element);\n\n      return [...nodes, destNode];\n    }\n\n    if (\n      (isSubstitutable && !containsAnotherToolTags) ||\n      (isBlockElement && !containsBlockElements && !containsAnotherToolTags)\n    ) {\n      return [...nodes, destNode, element];\n    }\n  }\n\n  /**\n   * Recursively divide HTML string to two types of nodes:\n   * 1. Block element\n   * 2. Document Fragments contained text and markup tags like a, b, i etc.\n   *\n   * @param {Node} wrapper - wrapper of paster HTML content\n   * @returns {Node[]}\n   */\n  private getNodes(wrapper: Node): Node[] {\n    const children = Array.from(wrapper.childNodes);\n    let elementNodeProcessingResult: Node[] | void;\n\n    const reducer = (nodes: Node[], node: Node): Node[] => {\n      if ($.isEmpty(node) && !$.isSingleTag(node as HTMLElement)) {\n        return nodes;\n      }\n\n      const lastNode = nodes[nodes.length - 1];\n\n      let destNode: Node = new DocumentFragment();\n\n      if (lastNode && $.isFragment(lastNode)) {\n        destNode = nodes.pop();\n      }\n\n      switch (node.nodeType) {\n        /**\n         * If node is HTML element:\n         * 1. Check if it is inline element\n         * 2. Check if it contains another block or substitutable elements\n         */\n        case Node.ELEMENT_NODE:\n          elementNodeProcessingResult = this.processElementNode(node, nodes, destNode);\n\n          if (elementNodeProcessingResult) {\n            return elementNodeProcessingResult;\n          }\n          break;\n\n        /**\n         * If node is text node, wrap it with DocumentFragment\n         */\n        case Node.TEXT_NODE:\n          destNode.appendChild(node);\n\n          return [...nodes, destNode];\n\n        default:\n          return [...nodes, destNode];\n      }\n\n      return [...nodes, ...Array.from(node.childNodes).reduce(reducer, [])];\n    };\n\n    return children.reduce(reducer, []);\n  }\n\n  /**\n   * Compose paste event with passed type and detail\n   *\n   * @param {string} type - event type\n   * @param {PasteEventDetail} detail - event detail\n   */\n  private composePasteEvent(type: string, detail: PasteEventDetail): PasteEvent {\n    return new CustomEvent(type, {\n      detail,\n    }) as PasteEvent;\n  }\n}\n\n"
  },
  {
    "path": "src/components/modules/readonly.ts",
    "content": "import Module from '../__module';\nimport { CriticalError } from '../errors/critical';\n\n/**\n * @module ReadOnly\n *\n * Has one important method:\n *    - {Function} toggleReadonly - Set read-only mode or toggle current state\n * @version 1.0.0\n * @typedef {ReadOnly} ReadOnly\n * @property {boolean} readOnlyEnabled - read-only state\n */\nexport default class ReadOnly extends Module {\n  /**\n   * Array of tools name which don't support read-only mode\n   */\n  private toolsDontSupportReadOnly: string[] = [];\n\n  /**\n   * Value to track read-only state\n   *\n   * @type {boolean}\n   */\n  private readOnlyEnabled = false;\n\n  /**\n   * Returns state of read only mode\n   */\n  public get isEnabled(): boolean {\n    return this.readOnlyEnabled;\n  }\n\n  /**\n   * Set initial state\n   */\n  public async prepare(): Promise<void> {\n    const { Tools } = this.Editor;\n    const { blockTools } = Tools;\n    const toolsDontSupportReadOnly: string[] = [];\n\n    Array\n      .from(blockTools.entries())\n      .forEach(([name, tool]) => {\n        if (!tool.isReadOnlySupported) {\n          toolsDontSupportReadOnly.push(name);\n        }\n      });\n\n    this.toolsDontSupportReadOnly = toolsDontSupportReadOnly;\n\n    if (this.config.readOnly && toolsDontSupportReadOnly.length > 0) {\n      this.throwCriticalError();\n    }\n\n    this.toggle(this.config.readOnly, true);\n  }\n\n  /**\n   * Set read-only mode or toggle current state\n   * Call all Modules `toggleReadOnly` method and re-render Editor\n   *\n   * @param state - (optional) read-only state or toggle\n   * @param isInitial - (optional) true when editor is initializing\n   */\n  public async toggle(state = !this.readOnlyEnabled, isInitial = false): Promise<boolean> {\n    if (state && this.toolsDontSupportReadOnly.length > 0) {\n      this.throwCriticalError();\n    }\n\n    const oldState = this.readOnlyEnabled;\n\n    this.readOnlyEnabled = state;\n\n    for (const name in this.Editor) {\n      /**\n       * Verify module has method `toggleReadOnly` method\n       */\n      if (!this.Editor[name].toggleReadOnly) {\n        continue;\n      }\n\n      /**\n       * set or toggle read-only state\n       */\n      this.Editor[name].toggleReadOnly(state);\n    }\n\n    /**\n     * If new state equals old one, do not re-render blocks\n     */\n    if (oldState === state) {\n      return this.readOnlyEnabled;\n    }\n\n    /**\n     * Do not re-render blocks if it's initial call\n     */\n    if (isInitial) {\n      return this.readOnlyEnabled;\n    }\n\n    /**\n     * Mutex for modifications observer to prevent onChange call when read-only mode is enabled\n     */\n    this.Editor.ModificationsObserver.disable();\n\n    /**\n     * Save current Editor Blocks and render again\n     */\n    const savedBlocks = await this.Editor.Saver.save();\n\n    await this.Editor.BlockManager.clear();\n    await this.Editor.Renderer.render(savedBlocks.blocks);\n\n    this.Editor.ModificationsObserver.enable();\n\n    return this.readOnlyEnabled;\n  }\n\n  /**\n   * Throws an error about tools which don't support read-only mode\n   */\n  private throwCriticalError(): never {\n    throw new CriticalError(\n      `To enable read-only mode all connected tools should support it. Tools ${this.toolsDontSupportReadOnly.join(', ')} don't support read-only mode.`\n    );\n  }\n}\n"
  },
  {
    "path": "src/components/modules/rectangleSelection.ts",
    "content": "/**\n * @class RectangleSelection\n * @classdesc Manages Block selection with mouse\n * @module RectangleSelection\n * @version 1.0.0\n */\nimport Module from '../__module';\nimport $ from '../dom';\n\nimport SelectionUtils from '../selection';\nimport Block from '../block';\nimport * as _ from '../utils';\n\n/**\n *\n */\nexport default class RectangleSelection extends Module {\n  /**\n   * CSS classes for the Block\n   *\n   * @returns {{wrapper: string, content: string}}\n   */\n  public static get CSS(): {[name: string]: string} {\n    return {\n      overlay: 'codex-editor-overlay',\n      overlayContainer: 'codex-editor-overlay__container',\n      rect: 'codex-editor-overlay__rectangle',\n      topScrollZone: 'codex-editor-overlay__scroll-zone--top',\n      bottomScrollZone: 'codex-editor-overlay__scroll-zone--bottom',\n    };\n  }\n\n  /**\n   * Using the selection rectangle\n   *\n   * @type {boolean}\n   */\n  private isRectSelectionActivated = false;\n\n  /**\n   *  Speed of Scrolling\n   */\n  private readonly SCROLL_SPEED: number = 3;\n\n  /**\n   *  Height of scroll zone on boundary of screen\n   */\n  private readonly HEIGHT_OF_SCROLL_ZONE = 40;\n\n  /**\n   *  Scroll zone type indicators\n   */\n  private readonly BOTTOM_SCROLL_ZONE = 1;\n  private readonly TOP_SCROLL_ZONE = 2;\n\n  /**\n   * Id of main button for event.button\n   */\n  private readonly MAIN_MOUSE_BUTTON = 0;\n\n  /**\n   *  Mouse is clamped\n   */\n  private mousedown = false;\n\n  /**\n   *  Is scrolling now\n   */\n  private isScrolling = false;\n\n  /**\n   *  Mouse is in scroll zone\n   */\n  private inScrollZone: number | null = null;\n\n  /**\n   *  Coords of rect\n   */\n  private startX = 0;\n  private startY = 0;\n  private mouseX = 0;\n  private mouseY = 0;\n\n  /**\n   * Selected blocks\n   */\n  private stackOfSelected: number[] = [];\n\n  /**\n   * Does the rectangle intersect blocks\n   */\n  private rectCrossesBlocks: boolean;\n\n  /**\n   * Selection rectangle\n   */\n  private overlayRectangle: HTMLDivElement;\n\n  /**\n   * Listener identifiers\n   */\n  private listenerIds: string[] = [];\n\n  /**\n   * Module Preparation\n   * Creating rect and hang handlers\n   */\n  public prepare(): void {\n    this.enableModuleBindings();\n  }\n\n  /**\n   * Init rect params\n   *\n   * @param {number} pageX - X coord of mouse\n   * @param {number} pageY - Y coord of mouse\n   */\n  public startSelection(pageX, pageY): void {\n    const elemWhereSelectionStart = document.elementFromPoint(pageX - window.pageXOffset, pageY - window.pageYOffset);\n\n    /**\n     * Don't clear selected block by clicks on the Block settings\n     * because we need to keep highlighting working block\n     */\n    const startsInsideToolbar = elemWhereSelectionStart.closest(`.${this.Editor.Toolbar.CSS.toolbar}`);\n\n    if (!startsInsideToolbar) {\n      this.Editor.BlockSelection.allBlocksSelected = false;\n      this.clearSelection();\n      this.stackOfSelected = [];\n    }\n\n    const selectorsToAvoid = [\n      `.${Block.CSS.content}`,\n      `.${this.Editor.Toolbar.CSS.toolbar}`,\n      `.${this.Editor.InlineToolbar.CSS.inlineToolbar}`,\n    ];\n\n    const startsInsideEditor = elemWhereSelectionStart.closest('.' + this.Editor.UI.CSS.editorWrapper);\n    const startsInSelectorToAvoid = selectorsToAvoid.some((selector) => !!elemWhereSelectionStart.closest(selector));\n\n    /**\n     * If selection starts outside of the editor or inside the blocks or on Editor UI elements, do not handle it\n     */\n    if (!startsInsideEditor || startsInSelectorToAvoid) {\n      return;\n    }\n\n    this.mousedown = true;\n    this.startX = pageX;\n    this.startY = pageY;\n  }\n\n  /**\n   * Clear all params to end selection\n   */\n  public endSelection(): void {\n    this.mousedown = false;\n    this.startX = 0;\n    this.startY = 0;\n    this.overlayRectangle.style.display = 'none';\n  }\n\n  /**\n   * is RectSelection Activated\n   */\n  public isRectActivated(): boolean {\n    return this.isRectSelectionActivated;\n  }\n\n  /**\n   * Mark that selection is end\n   */\n  public clearSelection(): void {\n    this.isRectSelectionActivated = false;\n  }\n\n  /**\n   * Sets Module necessary event handlers\n   */\n  private enableModuleBindings(): void {\n    const { container } = this.genHTML();\n\n    this.listeners.on(container, 'mousedown', (mouseEvent: MouseEvent) => {\n      this.processMouseDown(mouseEvent);\n    }, false);\n\n    this.listeners.on(document.body, 'mousemove', _.throttle((mouseEvent: MouseEvent) => {\n      this.processMouseMove(mouseEvent);\n    // eslint-disable-next-line @typescript-eslint/no-magic-numbers\n    }, 10), {\n      passive: true,\n    });\n\n    this.listeners.on(document.body, 'mouseleave', () => {\n      this.processMouseLeave();\n    });\n\n    this.listeners.on(window, 'scroll', _.throttle((mouseEvent: MouseEvent) => {\n      this.processScroll(mouseEvent);\n    // eslint-disable-next-line @typescript-eslint/no-magic-numbers\n    }, 10), {\n      passive: true,\n    });\n\n    this.listeners.on(document.body, 'mouseup', () => {\n      this.processMouseUp();\n    }, false);\n  }\n\n  /**\n   * Handle mouse down events\n   *\n   * @param {MouseEvent} mouseEvent - mouse event payload\n   */\n  private processMouseDown(mouseEvent: MouseEvent): void {\n    if (mouseEvent.button !== this.MAIN_MOUSE_BUTTON) {\n      return;\n    }\n\n    /**\n     * Do not enable the Rectangle Selection when mouse dragging started some editable input\n     * Used to prevent Rectangle Selection on Block Tune wrappers' inputs that also can be inside the Block\n     */\n    const startedFromContentEditable = (mouseEvent.target as Element).closest($.allInputsSelector) !== null;\n\n    if (!startedFromContentEditable) {\n      this.startSelection(mouseEvent.pageX, mouseEvent.pageY);\n    }\n  }\n\n  /**\n   * Handle mouse move events\n   *\n   * @param {MouseEvent} mouseEvent - mouse event payload\n   */\n  private processMouseMove(mouseEvent: MouseEvent): void {\n    this.changingRectangle(mouseEvent);\n    this.scrollByZones(mouseEvent.clientY);\n  }\n\n  /**\n   * Handle mouse leave\n   */\n  private processMouseLeave(): void {\n    this.clearSelection();\n    this.endSelection();\n  }\n\n  /**\n   * @param {MouseEvent} mouseEvent - mouse event payload\n   */\n  private processScroll(mouseEvent: MouseEvent): void {\n    this.changingRectangle(mouseEvent);\n  }\n\n  /**\n   * Handle mouse up\n   */\n  private processMouseUp(): void {\n    this.clearSelection();\n    this.endSelection();\n  }\n\n  /**\n   * Scroll If mouse in scroll zone\n   *\n   * @param {number} clientY - Y coord of mouse\n   */\n  private scrollByZones(clientY): void {\n    this.inScrollZone = null;\n    if (clientY <= this.HEIGHT_OF_SCROLL_ZONE) {\n      this.inScrollZone = this.TOP_SCROLL_ZONE;\n    }\n    if (document.documentElement.clientHeight - clientY <= this.HEIGHT_OF_SCROLL_ZONE) {\n      this.inScrollZone = this.BOTTOM_SCROLL_ZONE;\n    }\n\n    if (!this.inScrollZone) {\n      this.isScrolling = false;\n\n      return;\n    }\n\n    if (!this.isScrolling) {\n      this.scrollVertical(this.inScrollZone === this.TOP_SCROLL_ZONE ? -this.SCROLL_SPEED : this.SCROLL_SPEED);\n      this.isScrolling = true;\n    }\n  }\n\n  /**\n   * Generates required HTML elements\n   *\n   * @returns {Object<string, Element>}\n   */\n  private genHTML(): {container: Element; overlay: Element} {\n    const { UI } = this.Editor;\n\n    const container = UI.nodes.holder.querySelector('.' + UI.CSS.editorWrapper);\n    const overlay = $.make('div', RectangleSelection.CSS.overlay, {});\n    const overlayContainer = $.make('div', RectangleSelection.CSS.overlayContainer, {});\n    const overlayRectangle = $.make('div', RectangleSelection.CSS.rect, {});\n\n    overlayContainer.appendChild(overlayRectangle);\n    overlay.appendChild(overlayContainer);\n    container.appendChild(overlay);\n\n    this.overlayRectangle = overlayRectangle as HTMLDivElement;\n\n    return {\n      container,\n      overlay,\n    };\n  }\n\n  /**\n   * Activates scrolling if blockSelection is active and mouse is in scroll zone\n   *\n   * @param {number} speed - speed of scrolling\n   */\n  private scrollVertical(speed): void {\n    if (!(this.inScrollZone && this.mousedown)) {\n      return;\n    }\n    const lastOffset = window.pageYOffset;\n\n    window.scrollBy(0, speed);\n    this.mouseY += window.pageYOffset - lastOffset;\n    setTimeout(() => {\n      this.scrollVertical(speed);\n    }, 0);\n  }\n\n  /**\n   * Handles the change in the rectangle and its effect\n   *\n   * @param {MouseEvent} event - mouse event\n   */\n  private changingRectangle(event: MouseEvent): void {\n    if (!this.mousedown) {\n      return;\n    }\n\n    if (event.pageY !== undefined) {\n      this.mouseX = event.pageX;\n      this.mouseY = event.pageY;\n    }\n\n    const { rightPos, leftPos, index } = this.genInfoForMouseSelection();\n    // There is not new block in selection\n\n    const rectIsOnRighSideOfredactor = this.startX > rightPos && this.mouseX > rightPos;\n    const rectISOnLeftSideOfRedactor = this.startX < leftPos && this.mouseX < leftPos;\n\n    this.rectCrossesBlocks = !(rectIsOnRighSideOfredactor || rectISOnLeftSideOfRedactor);\n\n    if (!this.isRectSelectionActivated) {\n      this.rectCrossesBlocks = false;\n      this.isRectSelectionActivated = true;\n      this.shrinkRectangleToPoint();\n      this.overlayRectangle.style.display = 'block';\n    }\n\n    this.updateRectangleSize();\n\n    /**\n     * Hide Block Settings Toggler (along with the Toolbar) (if showed) when the Rectangle Selection is activated\n     */\n    this.Editor.Toolbar.close();\n\n    if (index === undefined) {\n      return;\n    }\n\n    this.trySelectNextBlock(index);\n    // For case, when rect is out from blocks\n    this.inverseSelection();\n\n    SelectionUtils.get().removeAllRanges();\n  }\n\n  /**\n   * Shrink rect to singular point\n   */\n  private shrinkRectangleToPoint(): void {\n    this.overlayRectangle.style.left = `${this.startX - window.pageXOffset}px`;\n    this.overlayRectangle.style.top = `${this.startY - window.pageYOffset}px`;\n    this.overlayRectangle.style.bottom = `calc(100% - ${this.startY - window.pageYOffset}px`;\n    this.overlayRectangle.style.right = `calc(100% - ${this.startX - window.pageXOffset}px`;\n  }\n\n  /**\n   * Select or unselect all of blocks in array if rect is out or in selectable area\n   */\n  private inverseSelection(): void {\n    const firstBlockInStack = this.Editor.BlockManager.getBlockByIndex(this.stackOfSelected[0]);\n    const isSelectedMode = firstBlockInStack.selected;\n\n    if (this.rectCrossesBlocks && !isSelectedMode) {\n      for (const it of this.stackOfSelected) {\n        this.Editor.BlockSelection.selectBlockByIndex(it);\n      }\n    }\n\n    if (!this.rectCrossesBlocks && isSelectedMode) {\n      for (const it of this.stackOfSelected) {\n        this.Editor.BlockSelection.unSelectBlockByIndex(it);\n      }\n    }\n  }\n\n  /**\n   * Updates size of rectangle\n   */\n  private updateRectangleSize(): void {\n    // Depending on the position of the mouse relative to the starting point,\n    // change this.e distance from the desired edge of the screen*/\n    if (this.mouseY >= this.startY) {\n      this.overlayRectangle.style.top = `${this.startY - window.pageYOffset}px`;\n      this.overlayRectangle.style.bottom = `calc(100% - ${this.mouseY - window.pageYOffset}px`;\n    } else {\n      this.overlayRectangle.style.bottom = `calc(100% - ${this.startY - window.pageYOffset}px`;\n      this.overlayRectangle.style.top = `${this.mouseY - window.pageYOffset}px`;\n    }\n\n    if (this.mouseX >= this.startX) {\n      this.overlayRectangle.style.left = `${this.startX - window.pageXOffset}px`;\n      this.overlayRectangle.style.right = `calc(100% - ${this.mouseX - window.pageXOffset}px`;\n    } else {\n      this.overlayRectangle.style.right = `calc(100% - ${this.startX - window.pageXOffset}px`;\n      this.overlayRectangle.style.left = `${this.mouseX - window.pageXOffset}px`;\n    }\n  }\n\n  /**\n   * Collects information needed to determine the behavior of the rectangle\n   *\n   * @returns {object} index - index next Block, leftPos - start of left border of Block, rightPos - right border\n   */\n  private genInfoForMouseSelection(): {index: number; leftPos: number; rightPos: number} {\n    const widthOfRedactor = document.body.offsetWidth;\n    const centerOfRedactor = widthOfRedactor / 2;\n    const Y = this.mouseY - window.pageYOffset;\n    const elementUnderMouse = document.elementFromPoint(centerOfRedactor, Y);\n    const blockInCurrentPos = this.Editor.BlockManager.getBlockByChildNode(elementUnderMouse);\n    let index;\n\n    if (blockInCurrentPos !== undefined) {\n      index = this.Editor.BlockManager.blocks.findIndex((block) => block.holder === blockInCurrentPos.holder);\n    }\n    const contentElement = this.Editor.BlockManager.lastBlock.holder.querySelector('.' + Block.CSS.content);\n    const centerOfBlock = Number.parseInt(window.getComputedStyle(contentElement).width, 10) / 2;\n    const leftPos = centerOfRedactor - centerOfBlock;\n    const rightPos = centerOfRedactor + centerOfBlock;\n\n    return {\n      index,\n      leftPos,\n      rightPos,\n    };\n  }\n\n  /**\n   * Select block with index index\n   *\n   * @param index - index of block in redactor\n   */\n  private addBlockInSelection(index): void {\n    if (this.rectCrossesBlocks) {\n      this.Editor.BlockSelection.selectBlockByIndex(index);\n    }\n    this.stackOfSelected.push(index);\n  }\n\n  /**\n   * Adds a block to the selection and determines which blocks should be selected\n   *\n   * @param {object} index - index of new block in the reactor\n   */\n  private trySelectNextBlock(index): void {\n    const sameBlock = this.stackOfSelected[this.stackOfSelected.length - 1] === index;\n    const sizeStack = this.stackOfSelected.length;\n    const down = 1, up = -1, undef = 0;\n\n    if (sameBlock) {\n      return;\n    }\n\n    const blockNumbersIncrease = this.stackOfSelected[sizeStack - 1] - this.stackOfSelected[sizeStack - 2] > 0;\n\n    let direction = undef;\n\n    if (sizeStack > 1) {\n      direction = blockNumbersIncrease ? down : up;\n    }\n\n    const selectionInDownDirection = index > this.stackOfSelected[sizeStack - 1] && direction === down;\n    const selectionInUpDirection = index < this.stackOfSelected[sizeStack - 1] && direction === up;\n    const generalSelection = selectionInDownDirection || selectionInUpDirection || direction === undef;\n    const reduction = !generalSelection;\n\n    // When the selection is too fast, some blocks do not have time to be noticed. Fix it.\n    if (!reduction && (index > this.stackOfSelected[sizeStack - 1] ||\n      this.stackOfSelected[sizeStack - 1] === undefined)) {\n      let ind = this.stackOfSelected[sizeStack - 1] + 1 || index;\n\n      for (ind; ind <= index; ind++) {\n        this.addBlockInSelection(ind);\n      }\n\n      return;\n    }\n\n    // for both directions\n    if (!reduction && (index < this.stackOfSelected[sizeStack - 1])) {\n      for (let ind = this.stackOfSelected[sizeStack - 1] - 1; ind >= index; ind--) {\n        this.addBlockInSelection(ind);\n      }\n\n      return;\n    }\n\n    if (!reduction) {\n      return;\n    }\n\n    let i = sizeStack - 1;\n    let cmp;\n\n    // cmp for different directions\n    if (index > this.stackOfSelected[sizeStack - 1]) {\n      cmp = (): boolean => index > this.stackOfSelected[i];\n    } else {\n      cmp = (): boolean => index < this.stackOfSelected[i];\n    }\n\n    // Remove blocks missed due to speed.\n    // cmp checks if we have removed all the necessary blocks\n    while (cmp()) {\n      if (this.rectCrossesBlocks) {\n        this.Editor.BlockSelection.unSelectBlockByIndex(this.stackOfSelected[i]);\n      }\n      this.stackOfSelected.pop();\n      i--;\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/modules/renderer.ts",
    "content": "import Module from '../__module';\nimport * as _ from '../utils';\nimport type { BlockId, BlockToolData, OutputBlockData } from '../../../types';\nimport type BlockToolAdapter from '../tools/block';\nimport type { StubData } from '../../tools/stub';\nimport type Block from '../block';\n\n/**\n * Module that responsible for rendering Blocks on editor initialization\n */\nexport default class Renderer extends Module {\n  /**\n   * Renders passed blocks as one batch\n   *\n   * @param blocksData - blocks to render\n   */\n  public async render(blocksData: OutputBlockData[]): Promise<void> {\n    return new Promise((resolve) => {\n      const { Tools, BlockManager } = this.Editor;\n\n      if (blocksData.length === 0) {\n        BlockManager.insert();\n      } else {\n        /**\n         * Create Blocks instances\n         */\n        const blocks = blocksData.map(({ type: tool, data, tunes, id }) => {\n          if (Tools.available.has(tool) === false) {\n            _.logLabeled(`Tool «${tool}» is not found. Check 'tools' property at the Editor.js config.`, 'warn');\n\n            data = this.composeStubDataForTool(tool, data, id);\n            tool = Tools.stubTool;\n          }\n\n          let block: Block;\n\n          try {\n            block = BlockManager.composeBlock({\n              id,\n              tool,\n              data,\n              tunes,\n            });\n          } catch (error) {\n            _.log(`Block «${tool}» skipped because of plugins error`, 'error', {\n              data,\n              error,\n            });\n\n            /**\n             * If tool throws an error during render, we should render stub instead of it\n             */\n            data = this.composeStubDataForTool(tool, data, id);\n            tool = Tools.stubTool;\n\n            block = BlockManager.composeBlock({\n              id,\n              tool,\n              data,\n              tunes,\n            });\n          }\n\n          return block;\n        });\n\n        /**\n         * Insert batch of Blocks\n         */\n        BlockManager.insertMany(blocks);\n      }\n\n      /**\n       * Wait till browser will render inserted Blocks and resolve a promise\n       */\n      window.requestIdleCallback(() => {\n        resolve();\n      }, { timeout: 2000 });\n    });\n  }\n\n  /**\n   * Create data for the Stub Tool that will be used instead of unavailable tool\n   *\n   * @param tool - unavailable tool name to stub\n   * @param data - data of unavailable block\n   * @param [id] - id of unavailable block\n   */\n  private composeStubDataForTool(tool: string, data: BlockToolData, id?: BlockId): StubData {\n    const { Tools } = this.Editor;\n\n    let title = tool;\n\n    if (Tools.unavailable.has(tool)) {\n      const toolboxSettings = (Tools.unavailable.get(tool) as BlockToolAdapter).toolbox;\n\n      if (toolboxSettings !== undefined && toolboxSettings[0].title !== undefined) {\n        title = toolboxSettings[0].title;\n      }\n    }\n\n    return {\n      savedData: {\n        id,\n        type: tool,\n        data,\n      },\n      title,\n    };\n  }\n}\n"
  },
  {
    "path": "src/components/modules/saver.ts",
    "content": "/**\n * Editor.js Saver\n *\n * @module Saver\n * @author Codex Team\n * @version 2.0.0\n */\nimport Module from '../__module';\nimport type { OutputData } from '../../../types';\nimport type { SavedData, ValidatedData } from '../../../types/data-formats';\nimport type Block from '../block';\nimport * as _ from '../utils';\nimport { sanitizeBlocks } from '../utils/sanitizer';\n\ndeclare const VERSION: string;\n\n/**\n * @classdesc This method reduces all Blocks asyncronically and calls Block's save method to extract data\n * @typedef {Saver} Saver\n * @property {Element} html - Editor HTML content\n * @property {string} json - Editor JSON output\n */\nexport default class Saver extends Module {\n  /**\n   * Composes new chain of Promises to fire them alternatelly\n   *\n   * @returns {OutputData}\n   */\n  public async save(): Promise<OutputData> {\n    const { BlockManager, Tools } = this.Editor;\n    const blocks = BlockManager.blocks,\n        chainData = [];\n\n    try {\n      blocks.forEach((block: Block) => {\n        chainData.push(this.getSavedData(block));\n      });\n\n      const extractedData = await Promise.all(chainData) as Array<Pick<SavedData, 'data' | 'tool'>>;\n      const sanitizedData = await sanitizeBlocks(extractedData, (name) => {\n        return Tools.blockTools.get(name).sanitizeConfig;\n      });\n\n      return this.makeOutput(sanitizedData);\n    } catch (e) {\n      _.logLabeled(`Saving failed due to the Error %o`, 'error', e);\n    }\n  }\n\n  /**\n   * Saves and validates\n   *\n   * @param {Block} block - Editor's Tool\n   * @returns {ValidatedData} - Tool's validated data\n   */\n  private async getSavedData(block: Block): Promise<ValidatedData> {\n    const blockData = await block.save();\n    const isValid = blockData && await block.validate(blockData.data);\n\n    return {\n      ...blockData,\n      isValid,\n    };\n  }\n\n  /**\n   * Creates output object with saved data, time and version of editor\n   *\n   * @param {ValidatedData} allExtractedData - data extracted from Blocks\n   * @returns {OutputData}\n   */\n  private makeOutput(allExtractedData): OutputData {\n    const blocks = [];\n\n    allExtractedData.forEach(({ id, tool, data, tunes, isValid }) => {\n      if (!isValid) {\n        _.log(`Block «${tool}» skipped because saved data is invalid`);\n\n        return;\n      }\n\n      /** If it was stub Block, get original data */\n      if (tool === this.Editor.Tools.stubTool) {\n        blocks.push(data);\n\n        return;\n      }\n\n      const output = {\n        id,\n        type: tool,\n        data,\n        ...!_.isEmpty(tunes) && {\n          tunes,\n        },\n      };\n\n      blocks.push(output);\n    });\n\n    return {\n      time: +new Date(),\n      blocks,\n      version: VERSION,\n    };\n  }\n}\n"
  },
  {
    "path": "src/components/modules/toolbar/blockSettings.ts",
    "content": "import Module from '../../__module';\nimport $ from '../../dom';\nimport SelectionUtils from '../../selection';\nimport type Block from '../../block';\nimport I18n from '../../i18n';\nimport { I18nInternalNS } from '../../i18n/namespace-internal';\nimport type Flipper from '../../flipper';\nimport type { MenuConfigItem } from '../../../../types/tools';\nimport { resolveAliases } from '../../utils/resolve-aliases';\nimport type { PopoverItemParams } from '../../utils/popover';\nimport { type Popover, PopoverDesktop, PopoverMobile, PopoverItemType } from '../../utils/popover';\nimport { PopoverEvent } from '@/types/utils/popover/popover-event';\nimport { isMobileScreen } from '../../utils';\nimport { EditorMobileLayoutToggled } from '../../events';\nimport { IconReplace } from '@codexteam/icons';\nimport { getConvertibleToolsForBlock } from '../../utils/blocks';\n\n/**\n * HTML Elements that used for BlockSettings\n */\ninterface BlockSettingsNodes {\n  /**\n   * Block Settings wrapper. Undefined when before \"make\" method called\n   */\n  wrapper: HTMLElement | undefined;\n}\n\n/**\n * Block Settings\n *\n *  @todo Make Block Settings no-module but a standalone class, like Toolbox\n */\nexport default class BlockSettings extends Module<BlockSettingsNodes> {\n  /**\n   * Module Events\n   */\n  public get events(): { opened: string; closed: string } {\n    return {\n      opened: 'block-settings-opened',\n      closed: 'block-settings-closed',\n    };\n  }\n\n  /**\n   * Block Settings CSS\n   */\n  public get CSS(): { [name: string]: string } {\n    return {\n      settings: 'ce-settings',\n    };\n  }\n\n  /**\n   * Opened state\n   */\n  public opened = false;\n\n  /**\n   * Getter for inner popover's flipper instance\n   *\n   * @todo remove once BlockSettings becomes standalone non-module class\n   */\n  public get flipper(): Flipper | undefined {\n    if (this.popover === null) {\n      return;\n    }\n\n    return 'flipper' in this.popover ? this.popover?.flipper : undefined;\n  }\n\n  /**\n   * Flag that indicates whether the `EditorMobileLayoutToggled` event listener is attached.\n   */\n  private hasMobileLayoutToggleListener = false;\n\n  /**\n   * Page selection utils\n   */\n  private selection: SelectionUtils = new SelectionUtils();\n\n  /**\n   * Popover instance. There is a util for vertical lists.\n   * Null until popover is not initialized\n   */\n  private popover: Popover | null = null;\n\n  /**\n   * Panel with block settings with 2 sections:\n   *  - Tool's Settings\n   *  - Default Settings [Move, Remove, etc]\n   */\n  public make(): void {\n    this.nodes.wrapper = $.make('div', [ this.CSS.settings ]);\n\n    if (import.meta.env.MODE === 'test') {\n      this.nodes.wrapper.setAttribute('data-cy', 'block-tunes');\n    }\n\n    this.eventsDispatcher.on(EditorMobileLayoutToggled, this.close);\n    this.hasMobileLayoutToggleListener = true;\n  }\n\n  /**\n   * Destroys module\n   */\n  public destroy(): void {\n    this.removeAllNodes();\n    this.listeners.destroy();\n\n    if (this.hasMobileLayoutToggleListener) {\n      this.eventsDispatcher.off(EditorMobileLayoutToggled, this.close);\n      this.hasMobileLayoutToggleListener = false;\n    }\n  }\n\n  /**\n   * Open Block Settings pane\n   *\n   * @param targetBlock - near which Block we should open BlockSettings\n   */\n  public async open(targetBlock: Block = this.Editor.BlockManager.currentBlock): Promise<void> {\n    this.opened = true;\n\n    /**\n     * If block settings contains any inputs, focus will be set there,\n     * so we need to save current selection to restore it after block settings is closed\n     */\n    this.selection.save();\n\n    /**\n     * Highlight content of a Block we are working with\n     */\n    this.Editor.BlockSelection.selectBlock(targetBlock);\n    this.Editor.BlockSelection.clearCache();\n\n    /** Get tool's settings data */\n    const { toolTunes, commonTunes } = targetBlock.getTunes();\n\n    /** Tell to subscribers that block settings is opened */\n    this.eventsDispatcher.emit(this.events.opened);\n\n    const PopoverClass = isMobileScreen() ? PopoverMobile : PopoverDesktop;\n\n    this.popover = new PopoverClass({\n      searchable: true,\n      items: await this.getTunesItems(targetBlock, commonTunes, toolTunes),\n      scopeElement: this.Editor.API.methods.ui.nodes.redactor,\n      messages: {\n        nothingFound: I18n.ui(I18nInternalNS.ui.popover, 'Nothing found'),\n        search: I18n.ui(I18nInternalNS.ui.popover, 'Filter'),\n      },\n    });\n\n    this.popover.on(PopoverEvent.Closed, this.onPopoverClose);\n\n    this.nodes.wrapper?.append(this.popover.getElement());\n\n    this.popover.show();\n  }\n\n  /**\n   * Returns root block settings element\n   */\n  public getElement(): HTMLElement | undefined {\n    return this.nodes.wrapper;\n  }\n\n  /**\n   * Close Block Settings pane\n   */\n  public close = (): void => {\n    if (!this.opened) {\n      return;\n    }\n\n    this.opened = false;\n\n    /**\n     * If selection is at editor on Block Settings closing,\n     * it means that caret placed at some editable element inside the Block Settings.\n     * Previously we have saved the selection, then open the Block Settings and set caret to the input\n     *\n     * So, we need to restore selection back to Block after closing the Block Settings\n     */\n    if (!SelectionUtils.isAtEditor) {\n      this.selection.restore();\n    }\n\n    this.selection.clearSaved();\n\n    /**\n     * Remove highlighted content of a Block we are working with\n     */\n    if (!this.Editor.CrossBlockSelection.isCrossBlockSelectionStarted && this.Editor.BlockManager.currentBlock) {\n      this.Editor.BlockSelection.unselectBlock(this.Editor.BlockManager.currentBlock);\n    }\n\n    /** Tell to subscribers that block settings is closed */\n    this.eventsDispatcher.emit(this.events.closed);\n\n    if (this.popover) {\n      this.popover.off(PopoverEvent.Closed, this.onPopoverClose);\n      this.popover.destroy();\n      this.popover.getElement().remove();\n      this.popover = null;\n    }\n  };\n\n  /**\n   * Returns list of items to be displayed in block tunes menu.\n   * Merges tool specific tunes, conversion menu and common tunes in one list in predefined order\n   *\n   * @param currentBlock –  block we are about to open block tunes for\n   * @param commonTunes – common tunes\n   * @param toolTunes - tool specific tunes\n   */\n  private async getTunesItems(currentBlock: Block, commonTunes: MenuConfigItem[], toolTunes?: MenuConfigItem[]): Promise<PopoverItemParams[]> {\n    const items = [] as MenuConfigItem[];\n\n    if (toolTunes !== undefined && toolTunes.length > 0) {\n      items.push(...toolTunes);\n      items.push({\n        type: PopoverItemType.Separator,\n      });\n    }\n\n    const allBlockTools = Array.from(this.Editor.Tools.blockTools.values());\n    const convertibleTools = await getConvertibleToolsForBlock(currentBlock, allBlockTools);\n    const convertToItems = convertibleTools.reduce((result, tool) => {\n      tool.toolbox.forEach((toolboxItem) => {\n        result.push({\n          icon: toolboxItem.icon,\n          title: I18n.t(I18nInternalNS.toolNames, toolboxItem.title),\n          name: tool.name,\n          closeOnActivate: true,\n          onActivate: async () => {\n            const { BlockManager, Caret, Toolbar } = this.Editor;\n\n            const newBlock = await BlockManager.convert(currentBlock, tool.name, toolboxItem.data);\n\n            Toolbar.close();\n\n            Caret.setToBlock(newBlock, Caret.positions.END);\n          },\n        });\n      });\n\n      return result;\n    }, []);\n\n    if (convertToItems.length > 0) {\n      items.push({\n        icon: IconReplace,\n        name: 'convert-to',\n        title: I18n.ui(I18nInternalNS.ui.popover, 'Convert to'),\n        children: {\n          searchable: true,\n          items: convertToItems,\n        },\n      });\n      items.push({\n        type: PopoverItemType.Separator,\n      });\n    }\n\n    items.push(...commonTunes);\n\n    return items.map(tune => this.resolveTuneAliases(tune));\n  }\n\n  /**\n   * Handles popover close event\n   */\n  private onPopoverClose = (): void => {\n    this.close();\n  };\n\n  /**\n   * Resolves aliases in tunes menu items\n   *\n   * @param item - item with resolved aliases\n   */\n  private resolveTuneAliases(item: MenuConfigItem): PopoverItemParams {\n    if (item.type === PopoverItemType.Separator || item.type === PopoverItemType.Html) {\n      return item;\n    }\n    const result = resolveAliases(item, { label: 'title' });\n\n    if (item.confirmation) {\n      result.confirmation = this.resolveTuneAliases(item.confirmation);\n    }\n\n    return result;\n  }\n}\n"
  },
  {
    "path": "src/components/modules/toolbar/index.ts",
    "content": "import Module from '../../__module';\nimport $, { calculateBaseline } from '../../dom';\nimport * as _ from '../../utils';\nimport I18n from '../../i18n';\nimport { I18nInternalNS } from '../../i18n/namespace-internal';\nimport * as tooltip from '../../utils/tooltip';\nimport type { ModuleConfig } from '../../../types-internal/module-config';\nimport type Block from '../../block';\nimport Toolbox, { ToolboxEvent } from '../../ui/toolbox';\nimport { IconMenu, IconPlus } from '@codexteam/icons';\nimport { BlockHovered } from '../../events/BlockHovered';\nimport { beautifyShortcut } from '../../utils';\nimport { getKeyboardKeyForCode } from '../../utils/keyboard';\n\n/**\n * @todo Tab on non-empty block should open Block Settings of the hoveredBlock (not where caret is set)\n *          - make Block Settings a standalone module\n * @todo - Keyboard-only mode bug:\n *         press Tab, flip to the Checkbox. press Enter (block will be added), Press Tab\n *         (Block Tunes will be opened with Move up focused), press Enter, press Tab ———— both Block Tunes and Toolbox will be opened\n * @todo TEST CASE - show toggler after opening and closing the Inline Toolbar\n * @todo TEST CASE - Click outside Editor holder should close Toolbar and Clear Focused blocks\n * @todo TEST CASE - Click inside Editor holder should close Toolbar and Clear Focused blocks\n * @todo TEST CASE - Click inside Redactor zone when Block Settings are opened:\n *                  - should close Block Settings\n *                  - should not close Toolbar\n *                  - should move Toolbar to the clicked Block\n * @todo TEST CASE - Toolbar should be closed on the Cross Block Selection\n * @todo TEST CASE - Toolbar should be closed on the Rectangle Selection\n * @todo TEST CASE - If Block Settings or Toolbox are opened, the Toolbar should not be moved by Bocks hovering\n */\n\n/**\n * HTML Elements used for Toolbar UI\n */\ninterface ToolbarNodes {\n  wrapper: HTMLElement | undefined;\n  content: HTMLElement | undefined;\n  actions: HTMLElement | undefined;\n\n  plusButton: HTMLElement | undefined;\n  settingsToggler: HTMLElement | undefined;\n}\n/**\n *\n * «Toolbar» is the node that moves up/down over current block\n *\n *  ______________________________________ Toolbar ____________________________________________\n * |                                                                                           |\n * |  ..................... Content .........................................................  |\n * |  .                                                   ........ Block Actions ...........   |\n * |  .                                                   .        [Open Settings]         .   |\n * |  .  [Plus Button]  [Toolbox: {Tool1}, {Tool2}]       .                                .   |\n * |  .                                                   .        [Settings Panel]        .   |\n * |  .                                                   ..................................   |\n * |  .......................................................................................  |\n * |                                                                                           |\n * |___________________________________________________________________________________________|\n *\n *\n * Toolbox — its an Element contains tools buttons. Can be shown by Plus Button.\n *\n *  _______________ Toolbox _______________\n * |                                       |\n * | [Header] [Image] [List] [Quote] ...   |\n * |_______________________________________|\n *\n *\n * Settings Panel — is an Element with block settings:\n *\n *   ____ Settings Panel ____\n *  | ...................... |\n *  | .   Tool Settings    . |\n *  | ...................... |\n *  | .  Default Settings  . |\n *  | ...................... |\n *  |________________________|\n *\n *\n * @class\n * @classdesc Toolbar module\n * @typedef {Toolbar} Toolbar\n * @property {object} nodes - Toolbar nodes\n * @property {Element} nodes.wrapper        - Toolbar main element\n * @property {Element} nodes.content        - Zone with Plus button and toolbox.\n * @property {Element} nodes.actions        - Zone with Block Settings and Remove Button\n * @property {Element} nodes.blockActionsButtons   - Zone with Block Buttons: [Settings]\n * @property {Element} nodes.plusButton     - Button that opens or closes Toolbox\n * @property {Element} nodes.toolbox        - Container for tools\n * @property {Element} nodes.settingsToggler - open/close Settings Panel button\n * @property {Element} nodes.settings          - Settings Panel\n * @property {Element} nodes.pluginSettings    - Plugin Settings section of Settings Panel\n * @property {Element} nodes.defaultSettings   - Default Settings section of Settings Panel\n */\nexport default class Toolbar extends Module<ToolbarNodes> {\n  /**\n   * Block near which we display the Toolbox\n   */\n  private hoveredBlock: Block;\n\n  /**\n   * Toolbox class instance\n   * It will be created in requestIdleCallback so it can be null in some period of time\n   */\n  private toolboxInstance: Toolbox | null = null;\n\n  /**\n   * @class\n   * @param moduleConfiguration - Module Configuration\n   * @param moduleConfiguration.config - Editor's config\n   * @param moduleConfiguration.eventsDispatcher - Editor's event dispatcher\n   */\n  constructor({ config, eventsDispatcher }: ModuleConfig) {\n    super({\n      config,\n      eventsDispatcher,\n    });\n  }\n\n  /**\n   * CSS styles\n   *\n   * @returns {object}\n   */\n  public get CSS(): { [name: string]: string } {\n    return {\n      toolbar: 'ce-toolbar',\n      content: 'ce-toolbar__content',\n      actions: 'ce-toolbar__actions',\n      actionsOpened: 'ce-toolbar__actions--opened',\n\n      toolbarOpened: 'ce-toolbar--opened',\n      openedToolboxHolderModifier: 'codex-editor--toolbox-opened',\n\n      plusButton: 'ce-toolbar__plus',\n      plusButtonShortcut: 'ce-toolbar__plus-shortcut',\n      settingsToggler: 'ce-toolbar__settings-btn',\n      settingsTogglerHidden: 'ce-toolbar__settings-btn--hidden',\n    };\n  }\n\n  /**\n   * Returns the Toolbar opening state\n   *\n   * @returns {boolean}\n   */\n  public get opened(): boolean {\n    return this.nodes.wrapper.classList.contains(this.CSS.toolbarOpened);\n  }\n\n  /**\n   * Public interface for accessing the Toolbox\n   */\n  public get toolbox(): {\n    opened: boolean | undefined; // undefined is for the case when Toolbox is not initialized yet\n    close: () => void;\n    open: () => void;\n    toggle: () => void;\n    hasFocus: () => boolean | undefined;\n    } {\n    return {\n      opened: this.toolboxInstance?.opened,\n      close: () => {\n        this.toolboxInstance?.close();\n      },\n      open: () => {\n        /**\n         * If Toolbox is not initialized yet, do nothing\n         */\n        if (this.toolboxInstance === null)  {\n          _.log('toolbox.open() called before initialization is finished', 'warn');\n\n          return;\n        }\n\n        /**\n         * Set current block to cover the case when the Toolbar showed near hovered Block but caret is set to another Block.\n         */\n        this.Editor.BlockManager.currentBlock = this.hoveredBlock;\n\n        this.toolboxInstance.open();\n      },\n      toggle: () => {\n        /**\n         * If Toolbox is not initialized yet, do nothing\n         */\n        if (this.toolboxInstance === null)  {\n          _.log('toolbox.toggle() called before initialization is finished', 'warn');\n\n          return;\n        }\n\n        this.toolboxInstance.toggle();\n      },\n      hasFocus: () => this.toolboxInstance?.hasFocus(),\n    };\n  }\n\n  /**\n   * Block actions appearance manipulations\n   */\n  private get blockActions(): { hide: () => void; show: () => void } {\n    return {\n      hide: (): void => {\n        this.nodes.actions.classList.remove(this.CSS.actionsOpened);\n      },\n      show: (): void => {\n        this.nodes.actions.classList.add(this.CSS.actionsOpened);\n      },\n    };\n  }\n\n  /**\n   * Methods for working with Block Tunes toggler\n   */\n  private get blockTunesToggler(): { hide: () => void; show: () => void } {\n    return {\n      hide: (): void => this.nodes.settingsToggler.classList.add(this.CSS.settingsTogglerHidden),\n      show: (): void => this.nodes.settingsToggler.classList.remove(this.CSS.settingsTogglerHidden),\n    };\n  }\n\n\n  /**\n   * Toggles read-only mode\n   *\n   * @param {boolean} readOnlyEnabled - read-only mode\n   */\n  public toggleReadOnly(readOnlyEnabled: boolean): void {\n    if (!readOnlyEnabled) {\n      window.requestIdleCallback(() => {\n        this.drawUI();\n        this.enableModuleBindings();\n      }, { timeout: 2000 });\n    } else {\n      this.destroy();\n      this.Editor.BlockSettings.destroy();\n      this.disableModuleBindings();\n    }\n  }\n\n  /**\n   * Move Toolbar to the passed (or current) Block\n   *\n   * @param block - block to move Toolbar near it\n   */\n  public moveAndOpen(block: Block = this.Editor.BlockManager.currentBlock): void {\n    /**\n     * Some UI elements creates inside requestIdleCallback, so the can be not ready yet\n     */\n    if (this.toolboxInstance === null)  {\n      _.log('Can\\'t open Toolbar since Editor initialization is not finished yet', 'warn');\n\n      return;\n    }\n\n    /**\n     * Close Toolbox when we move toolbar\n     */\n    if (this.toolboxInstance.opened) {\n      this.toolboxInstance.close();\n    }\n\n    if (this.Editor.BlockSettings.opened) {\n      this.Editor.BlockSettings.close();\n    }\n\n    /**\n     * If no one Block selected as a Current\n     */\n    if (!block) {\n      return;\n    }\n\n    this.hoveredBlock = block;\n\n    const targetBlockHolder = block.holder;\n    const { isMobile } = this.Editor.UI;\n\n\n    /**\n     * 1. Mobile:\n     *  - Toolbar at the bottom of the block\n     *\n     * 2. Desktop:\n     *   There are two cases of a toolbar position:\n     *      2.1 Toolbar is moved to the top of the block (+ padding top of the block)\n     *       - when the first input is far from the top of the block, for example in Image tool\n     *       - when block has no inputs\n     *      2.2 Toolbar is moved to the baseline of the first input\n     *       - when the first input is close to the top of the block\n     */\n    let toolbarY;\n    const MAX_OFFSET = 20;\n\n    /**\n     * Compute first input position\n     */\n    const firstInput = block.firstInput;\n    const targetBlockHolderRect = targetBlockHolder.getBoundingClientRect();\n    const firstInputRect = firstInput !== undefined ? firstInput.getBoundingClientRect() : null;\n\n    /**\n     * Compute the offset of the first input from the top of the block\n     */\n    const firstInputOffset = firstInputRect !== null ? firstInputRect.top - targetBlockHolderRect.top : null;\n\n    /**\n     * Check if the first input is far from the top of the block\n     */\n    const isFirstInputFarFromTop = firstInputOffset !== null ? firstInputOffset > MAX_OFFSET : undefined;\n\n    /**\n     * Case 1.\n     * On mobile — Toolbar at the bottom of Block\n     */\n    if (isMobile) {\n      toolbarY = targetBlockHolder.offsetTop + targetBlockHolder.offsetHeight;\n\n    /**\n     * Case 2.1\n     * On Desktop — without inputs or with the first input far from the top of the block\n     *            Toolbar should be moved to the top of the block\n     */\n    } else if (firstInput === undefined || isFirstInputFarFromTop) {\n      const pluginContentOffset = parseInt(window.getComputedStyle(block.pluginsContent).paddingTop);\n\n      const paddingTopBasedY = targetBlockHolder.offsetTop + pluginContentOffset;\n\n      toolbarY = paddingTopBasedY;\n\n    /**\n     * Case 2.2\n     * On Desktop — Toolbar should be moved to the baseline of the first input\n     */\n    } else {\n      const baseline = calculateBaseline(firstInput);\n      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n      const toolbarActionsHeight =  parseInt(window.getComputedStyle(this.nodes.plusButton!).height, 10);\n      /**\n       * Visual padding inside the SVG icon\n       */\n      const toolbarActionsPaddingBottom = 8;\n\n      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n      const baselineBasedY = targetBlockHolder.offsetTop + baseline - toolbarActionsHeight + toolbarActionsPaddingBottom + firstInputOffset!;\n\n      toolbarY = baselineBasedY;\n    }\n\n    /**\n     * Move Toolbar to the Top coordinate of Block\n     */\n    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n    this.nodes.wrapper!.style.top = `${Math.floor(toolbarY)}px`;\n\n    /**\n     * Do not show Block Tunes Toggler near single and empty block\n     */\n    if (this.Editor.BlockManager.blocks.length === 1 && block.isEmpty) {\n      this.blockTunesToggler.hide();\n    } else {\n      this.blockTunesToggler.show();\n    }\n\n    this.open();\n  }\n\n  /**\n   * Close the Toolbar\n   */\n  public close(): void {\n    if (this.Editor.ReadOnly.isEnabled) {\n      return;\n    }\n\n    this.nodes.wrapper?.classList.remove(this.CSS.toolbarOpened);\n\n    /** Close components */\n    this.blockActions.hide();\n    this.toolboxInstance?.close();\n    this.Editor.BlockSettings.close();\n    this.reset();\n  }\n\n  /**\n   * Reset the Toolbar position to prevent DOM height growth, for example after blocks deletion\n   */\n  private reset(): void {\n    this.nodes.wrapper.style.top = 'unset';\n  }\n\n  /**\n   * Open Toolbar with Plus Button and Actions\n   *\n   * @param {boolean} withBlockActions - by default, Toolbar opens with Block Actions.\n   *                                     This flag allows to open Toolbar without Actions.\n   */\n  private open(withBlockActions = true): void {\n    this.nodes.wrapper.classList.add(this.CSS.toolbarOpened);\n\n    if (withBlockActions) {\n      this.blockActions.show();\n    } else {\n      this.blockActions.hide();\n    }\n  }\n\n  /**\n   * Draws Toolbar elements\n   */\n  private async make(): Promise<void> {\n    this.nodes.wrapper = $.make('div', this.CSS.toolbar);\n    /**\n     * @todo detect test environment and add data-cy=\"toolbar\" to use it in tests instead of class name\n     */\n\n    /**\n     * Make Content Zone and Actions Zone\n     */\n    ['content', 'actions'].forEach((el) => {\n      this.nodes[el] = $.make('div', this.CSS[el]);\n    });\n\n    /**\n     * Actions will be included to the toolbar content so we can align in to the right of the content\n     */\n    $.append(this.nodes.wrapper, this.nodes.content);\n    $.append(this.nodes.content, this.nodes.actions);\n\n    /**\n     * Fill Content Zone:\n     *  - Plus Button\n     *  - Toolbox\n     */\n    this.nodes.plusButton = $.make('div', this.CSS.plusButton, {\n      innerHTML: IconPlus,\n    });\n    $.append(this.nodes.actions, this.nodes.plusButton);\n\n    this.readOnlyMutableListeners.on(this.nodes.plusButton, 'click', () => {\n      tooltip.hide(true);\n      this.plusButtonClicked();\n    }, false);\n\n    /**\n     * Add events to show/hide tooltip for plus button\n     */\n    const tooltipContent = $.make('div');\n\n    tooltipContent.appendChild(document.createTextNode(I18n.ui(I18nInternalNS.ui.toolbar.toolbox, 'Add')));\n    tooltipContent.appendChild($.make('div', this.CSS.plusButtonShortcut, {\n      textContent: '/',\n    }));\n\n    tooltip.onHover(this.nodes.plusButton, tooltipContent, {\n      hidingDelay: 400,\n    });\n\n    /**\n     * Fill Actions Zone:\n     *  - Settings Toggler\n     *  - Remove Block Button\n     *  - Settings Panel\n     */\n    this.nodes.settingsToggler = $.make('span', this.CSS.settingsToggler, {\n      innerHTML: IconMenu,\n    });\n\n    $.append(this.nodes.actions, this.nodes.settingsToggler);\n\n    const blockTunesTooltip = $.make('div');\n    const blockTunesTooltipEl = $.text(I18n.ui(I18nInternalNS.ui.blockTunes.toggler, 'Click to tune'));\n    const slashRealKey = await getKeyboardKeyForCode('Slash', '/');\n\n    blockTunesTooltip.appendChild(blockTunesTooltipEl);\n    blockTunesTooltip.appendChild($.make('div', this.CSS.plusButtonShortcut, {\n      textContent: beautifyShortcut(`CMD + ${slashRealKey}`),\n    }));\n\n    tooltip.onHover(this.nodes.settingsToggler, blockTunesTooltip, {\n      hidingDelay: 400,\n    });\n\n    /**\n     * Appending Toolbar components to itself\n     */\n    $.append(this.nodes.actions, this.makeToolbox());\n    $.append(this.nodes.actions, this.Editor.BlockSettings.getElement());\n\n    /**\n     * Append toolbar to the Editor\n     */\n    $.append(this.Editor.UI.nodes.wrapper, this.nodes.wrapper);\n  }\n\n  /**\n   * Creates the Toolbox instance and return it's rendered element\n   */\n  private makeToolbox(): Element {\n    /**\n     * Make the Toolbox\n     */\n    this.toolboxInstance = new Toolbox({\n      api: this.Editor.API.methods,\n      tools: this.Editor.Tools.blockTools,\n      i18nLabels: {\n        filter: I18n.ui(I18nInternalNS.ui.popover, 'Filter'),\n        nothingFound: I18n.ui(I18nInternalNS.ui.popover, 'Nothing found'),\n      },\n    });\n\n    this.toolboxInstance.on(ToolboxEvent.Opened, () => {\n      this.Editor.UI.nodes.wrapper.classList.add(this.CSS.openedToolboxHolderModifier);\n    });\n\n    this.toolboxInstance.on(ToolboxEvent.Closed, () => {\n      this.Editor.UI.nodes.wrapper.classList.remove(this.CSS.openedToolboxHolderModifier);\n    });\n\n    this.toolboxInstance.on(ToolboxEvent.BlockAdded, ({ block }) => {\n      const { BlockManager, Caret } = this.Editor;\n      const newBlock = BlockManager.getBlockById(block.id);\n\n      /**\n       * If the new block doesn't contain inputs, insert the new paragraph below\n       */\n      if (newBlock.inputs.length === 0) {\n        if (newBlock === BlockManager.lastBlock) {\n          BlockManager.insertAtEnd();\n          Caret.setToBlock(BlockManager.lastBlock);\n        } else {\n          Caret.setToBlock(BlockManager.nextBlock);\n        }\n      }\n    });\n\n    return this.toolboxInstance.getElement();\n  }\n\n\n  /**\n   * Handler for Plus Button\n   */\n  private plusButtonClicked(): void {\n    /**\n     * We need to update Current Block because user can click on the Plus Button (thanks to appearing by hover) without any clicks on editor\n     * In this case currentBlock will point last block\n     */\n    this.Editor.BlockManager.currentBlock = this.hoveredBlock;\n\n    this.toolboxInstance?.toggle();\n  }\n\n  /**\n   * Enable bindings\n   */\n  private enableModuleBindings(): void {\n    /**\n     * Settings toggler\n     *\n     * mousedown is used because on click selection is lost in Safari and FF\n     */\n    this.readOnlyMutableListeners.on(this.nodes.settingsToggler, 'mousedown', (e) => {\n      /**\n       * Stop propagation to prevent block selection clearance\n       *\n       * @see UI.documentClicked\n       */\n      e.stopPropagation();\n\n      this.settingsTogglerClicked();\n\n      if (this.toolboxInstance?.opened) {\n        this.toolboxInstance.close();\n      }\n\n      tooltip.hide(true);\n    }, true);\n\n    /**\n     * Subscribe to the 'block-hovered' event if current view is not mobile\n     *\n     * @see https://github.com/codex-team/editor.js/issues/1972\n     */\n    if (!_.isMobileScreen()) {\n      /**\n       * Subscribe to the 'block-hovered' event\n       */\n      this.eventsDispatcher.on(BlockHovered, (data) => {\n        /**\n         * Do not move toolbar if Block Settings or Toolbox opened\n         */\n        if (this.Editor.BlockSettings.opened || this.toolboxInstance?.opened) {\n          return;\n        }\n\n        this.moveAndOpen(data.block);\n      });\n    }\n  }\n\n  /**\n   * Disable bindings\n   */\n  private disableModuleBindings(): void {\n    this.readOnlyMutableListeners.clearAll();\n  }\n\n  /**\n   * Clicks on the Block Settings toggler\n   */\n  private settingsTogglerClicked(): void {\n    /**\n     * We need to update Current Block because user can click on toggler (thanks to appearing by hover) without any clicks on editor\n     * In this case currentBlock will point last block\n     */\n    this.Editor.BlockManager.currentBlock = this.hoveredBlock;\n\n    if (this.Editor.BlockSettings.opened) {\n      this.Editor.BlockSettings.close();\n    } else {\n      this.Editor.BlockSettings.open(this.hoveredBlock);\n    }\n  }\n\n  /**\n   * Draws Toolbar UI\n   *\n   * Toolbar contains BlockSettings and Toolbox.\n   * That's why at first we draw its components and then Toolbar itself\n   *\n   * Steps:\n   *  - Make Toolbar dependent components like BlockSettings, Toolbox and so on\n   *  - Make itself and append dependent nodes to itself\n   *\n   */\n  private drawUI(): void {\n    /**\n     * Make BlockSettings Panel\n     */\n    this.Editor.BlockSettings.make();\n\n    /**\n     * Make Toolbar\n     */\n    void this.make();\n  }\n\n  /**\n   * Removes all created and saved HTMLElements\n   * It is used in Read-Only mode\n   */\n  private destroy(): void {\n    this.removeAllNodes();\n    if (this.toolboxInstance) {\n      this.toolboxInstance.destroy();\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/modules/toolbar/inline.ts",
    "content": "/* eslint-disable @typescript-eslint/no-non-null-assertion */\nimport Module from '../../__module';\nimport $ from '../../dom';\nimport SelectionUtils from '../../selection';\nimport * as _ from '../../utils';\nimport type { InlineTool as IInlineTool } from '../../../../types';\nimport I18n from '../../i18n';\nimport { I18nInternalNS } from '../../i18n/namespace-internal';\nimport Shortcuts from '../../utils/shortcuts';\nimport type { ModuleConfig } from '../../../types-internal/module-config';\nimport { CommonInternalSettings } from '../../tools/base';\nimport type { Popover, PopoverItemHtmlParams, PopoverItemParams, WithChildren } from '../../utils/popover';\nimport { PopoverItemType } from '../../utils/popover';\nimport { PopoverInline } from '../../utils/popover/popover-inline';\nimport type InlineToolAdapter from 'src/components/tools/inline';\n\n/**\n * Inline Toolbar elements\n */\ninterface InlineToolbarNodes {\n  wrapper: HTMLElement | undefined;\n}\n\n/**\n * Inline toolbar with actions that modifies selected text fragment\n *\n * |¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯|\n * |   B  i [link] [mark]   |\n * |________________________|\n */\nexport default class InlineToolbar extends Module<InlineToolbarNodes> {\n  /**\n   * CSS styles\n   */\n  public CSS = {\n    inlineToolbar: 'ce-inline-toolbar',\n  };\n\n  /**\n   * State of inline toolbar\n   */\n  public opened = false;\n\n  /**\n   * Popover instance reference\n   */\n  private popover: Popover | null = null;\n\n  /**\n   * Margin above/below the Toolbar\n   */\n  // eslint-disable-next-line @typescript-eslint/no-magic-numbers\n  private readonly toolbarVerticalMargin: number = _.isMobileScreen() ? 20 : 6;\n\n  /**\n   * Currently visible tools instances\n   */\n  private tools: Map<InlineToolAdapter, IInlineTool> = new Map();\n\n  /**\n   * @param moduleConfiguration - Module Configuration\n   * @param moduleConfiguration.config - Editor's config\n   * @param moduleConfiguration.eventsDispatcher - Editor's event dispatcher\n   */\n  constructor({ config, eventsDispatcher }: ModuleConfig) {\n    super({\n      config,\n      eventsDispatcher,\n    });\n\n    window.requestIdleCallback(() => {\n      this.make();\n    }, { timeout: 2000 });\n  }\n\n  /**\n   *  Moving / appearance\n   *  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n   */\n\n  /**\n   * Shows Inline Toolbar if something is selected\n   *\n   * @param [needToClose] - pass true to close toolbar if it is not allowed.\n   *                                  Avoid to use it just for closing IT, better call .close() clearly.\n   */\n  public async tryToShow(needToClose = false): Promise<void> {\n    if (needToClose) {\n      this.close();\n    }\n\n    if (!this.allowedToShow()) {\n      return;\n    }\n\n    await this.open();\n\n    this.Editor.Toolbar.close();\n  }\n\n  /**\n   * Hides Inline Toolbar\n   */\n  public close(): void {\n    if (!this.opened) {\n      return;\n    }\n\n    for (const [tool, toolInstance] of this.tools) {\n      const shortcut = this.getToolShortcut(tool.name);\n\n      if (shortcut !== undefined) {\n        Shortcuts.remove(this.Editor.UI.nodes.redactor, shortcut);\n      }\n\n      /**\n       * @todo replace 'clear' with 'destroy'\n       */\n      if (_.isFunction(toolInstance.clear)) {\n        toolInstance.clear();\n      }\n    }\n\n    this.tools = new Map();\n\n    this.reset();\n    this.opened = false;\n\n    this.popover?.hide();\n    this.popover?.destroy();\n    this.popover = null;\n  }\n\n  /**\n   * Check if node is contained by Inline Toolbar\n   *\n   * @param {Node} node — node to check\n   */\n  public containsNode(node: Node): boolean {\n    if (this.nodes.wrapper === undefined) {\n      return false;\n    }\n\n    return this.nodes.wrapper.contains(node);\n  }\n\n  /**\n   * Removes UI and its components\n   */\n  public destroy(): void {\n    this.removeAllNodes();\n    this.popover?.destroy();\n    this.popover = null;\n  }\n\n  /**\n   * Making DOM\n   */\n  private make(): void {\n    this.nodes.wrapper = $.make('div', [\n      this.CSS.inlineToolbar,\n      ...(this.isRtl ? [ this.Editor.UI.CSS.editorRtlFix ] : []),\n    ]);\n\n    if (import.meta.env.MODE === 'test') {\n      this.nodes.wrapper.setAttribute('data-cy', 'inline-toolbar');\n    }\n\n    /**\n     * Append the inline toolbar to the editor.\n     */\n    $.append(this.Editor.UI.nodes.wrapper, this.nodes.wrapper);\n  }\n\n  /**\n   * Shows Inline Toolbar\n   */\n  private async open(): Promise<void> {\n    if (this.opened) {\n      return;\n    }\n\n    /**\n     * Show Inline Toolbar\n     */\n\n    this.opened = true;\n\n    if (this.popover !== null) {\n      this.popover.destroy();\n    }\n\n    this.createToolsInstances();\n\n    const popoverItems = await this.getPopoverItems();\n\n    this.popover = new PopoverInline({\n      items: popoverItems,\n      scopeElement: this.Editor.API.methods.ui.nodes.redactor,\n      messages: {\n        nothingFound: I18n.ui(I18nInternalNS.ui.popover, 'Nothing found'),\n        search: I18n.ui(I18nInternalNS.ui.popover, 'Filter'),\n      },\n    });\n\n    this.move(this.popover.size.width);\n\n    this.nodes.wrapper?.append(this.popover.getElement());\n\n    this.popover.show();\n  }\n\n  /**\n   * Move Toolbar to the selected text\n   *\n   * @param popoverWidth - width of the toolbar popover\n   */\n  private move(popoverWidth: number): void {\n    const selectionRect = SelectionUtils.rect as DOMRect;\n    const wrapperOffset = this.Editor.UI.nodes.wrapper.getBoundingClientRect();\n    const newCoords = {\n      x: selectionRect.x - wrapperOffset.x,\n      y: selectionRect.y +\n        selectionRect.height -\n        // + window.scrollY\n        wrapperOffset.top +\n        this.toolbarVerticalMargin,\n    };\n\n    const realRightCoord = newCoords.x + popoverWidth + wrapperOffset.x;\n\n    /**\n     * Prevent InlineToolbar from overflowing the content zone on the right side\n     */\n    if (realRightCoord > this.Editor.UI.contentRect.right) {\n      newCoords.x = this.Editor.UI.contentRect.right -popoverWidth - wrapperOffset.x;\n    }\n\n    this.nodes.wrapper!.style.left = Math.floor(newCoords.x) + 'px';\n    this.nodes.wrapper!.style.top = Math.floor(newCoords.y) + 'px';\n  }\n\n  /**\n   * Clear orientation classes and reset position\n   */\n  private reset(): void {\n    this.nodes.wrapper!.style.left = '0';\n    this.nodes.wrapper!.style.top = '0';\n  }\n\n  /**\n   * Need to show Inline Toolbar or not\n   */\n  private allowedToShow(): boolean {\n    /**\n     * Tags conflicts with window.selection function.\n     * Ex. IMG tag returns null (Firefox) or Redactors wrapper (Chrome)\n     */\n    const tagsConflictsWithSelection = ['IMG', 'INPUT'];\n    const currentSelection = SelectionUtils.get();\n    const selectedText = SelectionUtils.text;\n\n    // old browsers\n    if (!currentSelection || !currentSelection.anchorNode) {\n      return false;\n    }\n\n    // empty selection\n    if (currentSelection.isCollapsed || selectedText.length < 1) {\n      return false;\n    }\n\n    const target = !$.isElement(currentSelection.anchorNode)\n      ? currentSelection.anchorNode.parentElement\n      : currentSelection.anchorNode;\n\n    if (target === null) {\n      return false;\n    }\n\n    if (currentSelection !== null && tagsConflictsWithSelection.includes(target.tagName)) {\n      return false;\n    }\n\n    /**\n     * Check if there is at leas one tool enabled by current Block's Tool\n     */\n    const currentBlock = this.Editor.BlockManager.getBlock(currentSelection.anchorNode as HTMLElement);\n\n    if (!currentBlock) {\n      return false;\n    }\n\n    /**\n     * Check that at least one tool is available for the current block\n     */\n    const toolsAvailable = this.getTools();\n    const isAtLeastOneToolAvailable = toolsAvailable.some((tool) => currentBlock.tool.inlineTools.has(tool.name));\n\n    if (isAtLeastOneToolAvailable === false) {\n      return false;\n    }\n\n    /**\n     * Inline toolbar will be shown only if the target is contenteditable\n     * In Read-Only mode, the target should be contenteditable with \"false\" value\n     */\n    const contenteditable = target.closest('[contenteditable]');\n\n    return contenteditable !== null;\n  }\n\n  /**\n   *  Working with Tools\n   *  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n   */\n\n  /**\n   * Returns tools that are available for current block\n   *\n   * Used to check if Inline Toolbar could be shown\n   * and to render tools in the Inline Toolbar\n   */\n  private getTools(): InlineToolAdapter[] {\n    const currentBlock = this.Editor.BlockManager.currentBlock;\n\n    if (!currentBlock) {\n      return [];\n    }\n\n    const inlineTools = Array.from(currentBlock.tool.inlineTools.values());\n\n    return inlineTools.filter((tool) => {\n      /**\n       * We support inline tools in read only mode.\n       * Such tools should have isReadOnlySupported flag set to true\n       */\n      if (this.Editor.ReadOnly.isEnabled && tool.isReadOnlySupported !== true) {\n        return false;\n      }\n\n      return true;\n    });\n  }\n\n  /**\n   * Constructs tools instances and saves them to this.tools\n   */\n  private createToolsInstances(): void {\n    this.tools = new Map();\n\n    const tools = this.getTools();\n\n    tools.forEach((tool) => {\n      const instance = tool.create();\n\n      this.tools.set(tool, instance);\n    });\n  }\n\n  /**\n   * Returns Popover Items for tools segregated by their appearance type: regular items and custom html elements.\n   */\n  private async getPopoverItems(): Promise<PopoverItemParams[]> {\n    const popoverItems = [] as PopoverItemParams[];\n\n    let i = 0;\n\n    for (const [tool, instance] of this.tools) {\n      const renderedTool = await instance.render();\n\n      /** Enable tool shortcut */\n      const shortcut = this.getToolShortcut(tool.name);\n\n      if (shortcut !== undefined) {\n        try {\n          this.enableShortcuts(tool.name, shortcut);\n        } catch (e) {}\n      }\n\n      const shortcutBeautified = shortcut !== undefined ? _.beautifyShortcut(shortcut) : undefined;\n\n      const toolTitle = I18n.t(\n        I18nInternalNS.toolNames,\n        tool.title || _.capitalize(tool.name)\n      );\n\n      [ renderedTool ].flat().forEach((item) => {\n        const commonPopoverItemParams = {\n          name: tool.name,\n          onActivate: () => {\n            this.toolClicked(instance);\n          },\n          hint: {\n            title: toolTitle,\n            description: shortcutBeautified,\n          },\n        } as PopoverItemParams;\n\n        if ($.isElement(item)) {\n          /**\n           * Deprecated way to add custom html elements to the Inline Toolbar\n           */\n\n          const popoverItem = {\n            ...commonPopoverItemParams,\n            element: item,\n            type: PopoverItemType.Html,\n          } as PopoverItemParams;\n\n          /**\n           * If tool specifies actions in deprecated manner, append them as children\n           */\n          if (_.isFunction(instance.renderActions)) {\n            const actions = instance.renderActions();\n\n            (popoverItem as WithChildren<PopoverItemHtmlParams>).children = {\n              isOpen: instance.checkState?.(SelectionUtils.get()),\n              /** Disable keyboard navigation in actions, as it might conflict with enter press handling */\n              isFlippable: false,\n              items: [\n                {\n                  type: PopoverItemType.Html,\n                  element: actions,\n                },\n              ],\n            };\n          } else {\n            /**\n             * Legacy inline tools might perform some UI mutating logic in checkState method, so, call it just in case\n             */\n            instance.checkState?.(SelectionUtils.get());\n          }\n\n          popoverItems.push(popoverItem);\n        } else if (item.type === PopoverItemType.Html) {\n          /**\n           * Actual way to add custom html elements to the Inline Toolbar\n           */\n          popoverItems.push({\n            ...commonPopoverItemParams,\n            ...item,\n            type: PopoverItemType.Html,\n          });\n        } else if (item.type === PopoverItemType.Separator) {\n          /**\n           * Separator item\n           */\n          popoverItems.push({\n            type: PopoverItemType.Separator,\n          });\n        } else {\n          /**\n           * Default item\n           */\n          const popoverItem = {\n            ...commonPopoverItemParams,\n            ...item,\n            type: PopoverItemType.Default,\n          } as PopoverItemParams;\n\n          /**\n           * Prepend the separator if item has children and not the first one\n           */\n          if ('children' in popoverItem && i !== 0) {\n            popoverItems.push({\n              type: PopoverItemType.Separator,\n            });\n          }\n\n          popoverItems.push(popoverItem);\n\n          /**\n           * Append a separator after the item if it has children and not the last one\n           */\n          if ('children' in popoverItem && i < this.tools.size - 1) {\n            popoverItems.push({\n              type: PopoverItemType.Separator,\n            });\n          }\n        }\n      });\n\n      i++;\n    }\n\n    return popoverItems;\n  }\n\n  /**\n   * Get shortcut name for tool\n   *\n   * @param toolName — Tool name\n   */\n  private getToolShortcut(toolName: string): string | undefined {\n    const { Tools } = this.Editor;\n\n    /**\n     * Enable shortcuts\n     * Ignore tool that doesn't have shortcut or empty string\n     */\n    const tool = Tools.inlineTools.get(toolName);\n\n    /**\n     * 1) For internal tools, check public getter 'shortcut'\n     * 2) For external tools, check tool's settings\n     * 3) If shortcut is not set in settings, check Tool's public property\n     */\n    const internalTools = Tools.internal.inlineTools;\n\n    if (Array.from(internalTools.keys()).includes(toolName)) {\n      return this.inlineTools[toolName][CommonInternalSettings.Shortcut];\n    }\n\n    return tool?.shortcut;\n  }\n\n  /**\n   * Enable Tool shortcut with Editor Shortcuts Module\n   *\n   * @param toolName - tool name\n   * @param shortcut - shortcut according to the ShortcutData Module format\n   */\n  private enableShortcuts(toolName: string, shortcut: string): void {\n    Shortcuts.add({\n      name: shortcut,\n      handler: (event) => {\n        const { currentBlock } = this.Editor.BlockManager;\n\n        /**\n         * Editor is not focused\n         */\n        if (!currentBlock) {\n          return;\n        }\n\n        /**\n         * We allow to fire shortcut with empty selection (isCollapsed=true)\n         * it can be used by tools like «Mention» that works without selection:\n         * Example: by SHIFT+@ show dropdown and insert selected username\n         */\n        // if (SelectionUtils.isCollapsed) return;\n\n        if (!currentBlock.tool.enabledInlineTools) {\n          return;\n        }\n\n        event.preventDefault();\n\n        this.popover?.activateItemByName(toolName);\n      },\n      /**\n       * We need to bind shortcut to the document to make it work in read-only mode\n       */\n      on: document,\n    });\n  }\n\n  /**\n   * Inline Tool button clicks\n   *\n   * @param tool - Tool's instance\n   */\n  private toolClicked(tool: IInlineTool): void {\n    const range = SelectionUtils.range;\n\n    tool.surround?.(range);\n    this.checkToolsState();\n  }\n\n  /**\n   * Check Tools` state by selection\n   */\n  private checkToolsState(): void {\n    this.tools?.forEach((toolInstance) => {\n      toolInstance.checkState?.(SelectionUtils.get());\n    });\n  }\n\n  /**\n   * Get inline tools tools\n   * Tools that has isInline is true\n   */\n  private get inlineTools(): { [name: string]: IInlineTool } {\n    const result = {} as  { [name: string]: IInlineTool } ;\n\n    Array\n      .from(this.Editor.Tools.inlineTools.entries())\n      .forEach(([name, tool]) => {\n        result[name] = tool.create();\n      });\n\n    return result;\n  }\n}\n"
  },
  {
    "path": "src/components/modules/tools.ts",
    "content": "import Paragraph from '@editorjs/paragraph';\nimport Module from '../__module';\nimport * as _ from '../utils';\nimport type { SanitizerConfig, ToolConfig, ToolConstructable, ToolSettings } from '../../../types';\nimport BoldInlineTool from '../inline-tools/inline-tool-bold';\nimport ItalicInlineTool from '../inline-tools/inline-tool-italic';\nimport LinkInlineTool from '../inline-tools/inline-tool-link';\nimport ConvertInlineTool from '../inline-tools/inline-tool-convert';\nimport Stub from '../../tools/stub';\nimport ToolsFactory from '../tools/factory';\nimport type InlineToolAdapter from '../tools/inline';\nimport type BlockToolAdapter from '../tools/block';\nimport type BlockTuneAdapter from '../tools/tune';\nimport MoveDownTune from '../block-tunes/block-tune-move-down';\nimport DeleteTune from '../block-tunes/block-tune-delete';\nimport MoveUpTune from '../block-tunes/block-tune-move-up';\nimport ToolsCollection from '../tools/collection';\n\n/**\n * @module Editor.js Tools Submodule\n *\n * Creates Instances from Plugins and binds external config to the instances\n */\n\n/**\n * Modules that works with tools classes\n */\nexport default class Tools extends Module {\n  /**\n   * Name of Stub Tool\n   * Stub Tool is used to substitute unavailable block Tools and store their data\n   *\n   * @type {string}\n   */\n  public stubTool = 'stub';\n\n  /**\n   * Returns available Tools\n   */\n  public get available(): ToolsCollection {\n    return this.toolsAvailable;\n  }\n\n  /**\n   * Returns unavailable Tools\n   */\n  public get unavailable(): ToolsCollection {\n    return this.toolsUnavailable;\n  }\n\n  /**\n   * Return Tools for the Inline Toolbar\n   */\n  public get inlineTools(): ToolsCollection<InlineToolAdapter> {\n    return this.available.inlineTools;\n  }\n\n  /**\n   * Return editor block tools\n   */\n  public get blockTools(): ToolsCollection<BlockToolAdapter> {\n    return this.available.blockTools;\n  }\n\n  /**\n   * Return available Block Tunes\n   *\n   * @returns {object} - object of Inline Tool's classes\n   */\n  public get blockTunes(): ToolsCollection<BlockTuneAdapter> {\n    return this.available.blockTunes;\n  }\n\n  /**\n   * Returns default Tool object\n   */\n  public get defaultTool(): BlockToolAdapter {\n    return this.blockTools.get(this.config.defaultBlock);\n  }\n\n  /**\n   * Tools objects factory\n   */\n  private factory: ToolsFactory;\n\n  /**\n   * Tools` classes available to use\n   */\n  private readonly toolsAvailable: ToolsCollection = new ToolsCollection();\n\n  /**\n   * Tools` classes not available to use because of preparation failure\n   */\n  private readonly toolsUnavailable: ToolsCollection = new ToolsCollection();\n\n  /**\n   * Returns internal tools\n   */\n  public get internal(): ToolsCollection {\n    return this.available.internalTools;\n  }\n\n  /**\n   * Creates instances via passed or default configuration\n   *\n   * @returns {Promise<void>}\n   */\n  public async prepare(): Promise<void> {\n    this.validateTools();\n\n    /**\n     * Assign internal tools\n     */\n    this.config.tools = _.deepMerge({}, this.internalTools, this.config.tools);\n\n    if (!Object.prototype.hasOwnProperty.call(this.config, 'tools') || Object.keys(this.config.tools).length === 0) {\n      throw Error('Can\\'t start without tools');\n    }\n\n    const config = this.prepareConfig();\n\n    this.factory = new ToolsFactory(config, this.config, this.Editor.API);\n\n    /**\n     * getting classes that has prepare method\n     */\n    const sequenceData = this.getListOfPrepareFunctions(config);\n\n    /**\n     * if sequence data contains nothing then resolve current chain and run other module prepare\n     */\n    if (sequenceData.length === 0) {\n      return Promise.resolve();\n    }\n\n    /**\n     * to see how it works {@link '../utils.ts#sequence'}\n     */\n    await _.sequence(sequenceData, (data: { toolName: string }) => {\n      this.toolPrepareMethodSuccess(data);\n    }, (data: { toolName: string }) => {\n      this.toolPrepareMethodFallback(data);\n    });\n\n    this.prepareBlockTools();\n  }\n\n  /**\n   * Return general Sanitizer config for all inline tools\n   */\n  @_.cacheable\n  public getAllInlineToolsSanitizeConfig(): SanitizerConfig {\n    const config: SanitizerConfig = {} as SanitizerConfig;\n\n    Array.from(this.inlineTools.values())\n      .forEach(inlineTool => {\n        Object.assign(config, inlineTool.sanitizeConfig);\n      });\n\n    return config;\n  }\n\n  /**\n   * Calls each Tool reset method to clean up anything set by Tool\n   */\n  public destroy(): void {\n    Object.values(this.available).forEach(async tool => {\n      if (_.isFunction(tool.reset)) {\n        await tool.reset();\n      }\n    });\n  }\n\n  /**\n   * Returns internal tools\n   * Includes Bold, Italic, Link and Paragraph\n   */\n  private get internalTools(): { [toolName: string]: ToolConstructable | ToolSettings & { isInternal?: boolean } } {\n    return {\n      convertTo: {\n        class: ConvertInlineTool,\n        isInternal: true,\n      },\n      link: {\n        class: LinkInlineTool,\n        isInternal: true,\n      },\n      bold: {\n        class: BoldInlineTool,\n        isInternal: true,\n      },\n      italic: {\n        class: ItalicInlineTool,\n        isInternal: true,\n      },\n      paragraph: {\n        class: Paragraph,\n        inlineToolbar: true,\n        isInternal: true,\n      },\n      stub: {\n        class: Stub,\n        isInternal: true,\n      },\n      moveUp: {\n        class: MoveUpTune,\n        isInternal: true,\n      },\n      delete: {\n        class: DeleteTune,\n        isInternal: true,\n      },\n      moveDown: {\n        class: MoveDownTune,\n        isInternal: true,\n      },\n    };\n  }\n\n  /**\n   * Tool prepare method success callback\n   *\n   * @param {object} data - append tool to available list\n   */\n  private toolPrepareMethodSuccess(data: { toolName: string }): void {\n    const tool = this.factory.get(data.toolName);\n\n    if (tool.isInline()) {\n      /**\n       * Some Tools validation\n       */\n      const inlineToolRequiredMethods = [ 'render' ];\n      const notImplementedMethods = inlineToolRequiredMethods.filter((method) => !tool.create()[method]);\n\n      if (notImplementedMethods.length) {\n        _.log(\n          `Incorrect Inline Tool: ${tool.name}. Some of required methods is not implemented %o`,\n          'warn',\n          notImplementedMethods\n        );\n\n        this.toolsUnavailable.set(tool.name, tool);\n\n        return;\n      }\n    }\n\n    this.toolsAvailable.set(tool.name, tool);\n  }\n\n  /**\n   * Tool prepare method fail callback\n   *\n   * @param {object} data - append tool to unavailable list\n   */\n  private toolPrepareMethodFallback(data: { toolName: string }): void {\n    this.toolsUnavailable.set(data.toolName, this.factory.get(data.toolName));\n  }\n\n  /**\n   * Binds prepare function of plugins with user or default config\n   *\n   * @returns {Array} list of functions that needs to be fired sequentially\n   * @param config - tools config\n   */\n  private getListOfPrepareFunctions(config: {[name: string]: ToolSettings}): {\n    function: (data: { toolName: string; config: ToolConfig }) => void | Promise<void>;\n    data: { toolName: string; config: ToolConfig };\n  }[] {\n    const toolPreparationList: {\n      function: (data: { toolName: string }) => void | Promise<void>;\n      data: { toolName: string; config: ToolConfig };\n    }[] = [];\n\n    Object\n      .entries(config)\n      .forEach(([toolName, settings]) => {\n        toolPreparationList.push({\n          // eslint-disable-next-line @typescript-eslint/no-empty-function\n          function: _.isFunction(settings.class.prepare) ? settings.class.prepare : (): void => {},\n          data: {\n            toolName,\n            config: settings.config,\n          },\n        });\n      });\n\n    return toolPreparationList;\n  }\n\n  /**\n   * Assign enabled Inline Tools and Block Tunes for Block Tool\n   */\n  private prepareBlockTools(): void {\n    Array.from(this.blockTools.values()).forEach(tool => {\n      this.assignInlineToolsToBlockTool(tool);\n      this.assignBlockTunesToBlockTool(tool);\n    });\n  }\n\n  /**\n   * Assign enabled Inline Tools for Block Tool\n   *\n   * @param tool - Block Tool\n   */\n  private assignInlineToolsToBlockTool(tool: BlockToolAdapter): void {\n    /**\n     * If common inlineToolbar property is false no Inline Tools should be assigned\n     */\n    if (this.config.inlineToolbar === false) {\n      return;\n    }\n\n    /**\n     * If user pass just 'true' for tool, get common inlineToolbar settings\n     * - if common settings is an array, use it\n     * - if common settings is 'true' or not specified, get default order\n     */\n    if (tool.enabledInlineTools === true) {\n      tool.inlineTools = new ToolsCollection<InlineToolAdapter>(\n        Array.isArray(this.config.inlineToolbar)\n          ? this.config.inlineToolbar.map(name => [name, this.inlineTools.get(name)])\n          /**\n           * If common settings is 'true' or not specified (will be set as true at core.ts), get the default order\n           */\n          : Array.from(this.inlineTools.entries())\n      );\n\n      return;\n    }\n\n    /**\n     * If user pass the list of inline tools for the particular tool, return it.\n     */\n    if (Array.isArray(tool.enabledInlineTools)) {\n      tool.inlineTools = new ToolsCollection<InlineToolAdapter>(\n        /** Prepend ConvertTo Inline Tool */\n        ['convertTo', ...tool.enabledInlineTools].map(name => [name, this.inlineTools.get(name)])\n      );\n    }\n  }\n\n  /**\n   * Assign enabled Block Tunes for Block Tool\n   *\n   * @param tool — Block Tool\n   */\n  private assignBlockTunesToBlockTool(tool: BlockToolAdapter): void {\n    if (tool.enabledBlockTunes === false) {\n      return;\n    }\n\n    if (Array.isArray(tool.enabledBlockTunes)) {\n      const userTunes = new ToolsCollection<BlockTuneAdapter>(\n        tool.enabledBlockTunes.map(name => [name, this.blockTunes.get(name)])\n      );\n\n      tool.tunes = new ToolsCollection<BlockTuneAdapter>([...userTunes, ...this.blockTunes.internalTools]);\n\n      return;\n    }\n\n    if (Array.isArray(this.config.tunes)) {\n      const userTunes = new ToolsCollection<BlockTuneAdapter>(\n        this.config.tunes.map(name => [name, this.blockTunes.get(name)])\n      );\n\n      tool.tunes = new ToolsCollection<BlockTuneAdapter>([...userTunes, ...this.blockTunes.internalTools]);\n\n      return;\n    }\n\n    tool.tunes = this.blockTunes.internalTools;\n  }\n\n  /**\n   * Validate Tools configuration objects and throw Error for user if it is invalid\n   */\n  private validateTools(): void {\n    /**\n     * Check Tools for a class containing\n     */\n    for (const toolName in this.config.tools) {\n      if (Object.prototype.hasOwnProperty.call(this.config.tools, toolName)) {\n        if (toolName in this.internalTools) {\n          return;\n        }\n\n        const tool = this.config.tools[toolName];\n\n        if (!_.isFunction(tool) && !_.isFunction((tool as ToolSettings).class)) {\n          throw Error(\n            `Tool «${toolName}» must be a constructor function or an object with function in the «class» property`\n          );\n        }\n      }\n    }\n  }\n\n  /**\n   * Unify tools config\n   */\n  private prepareConfig(): {[name: string]: ToolSettings} {\n    const config: {[name: string]: ToolSettings} = {};\n\n    /**\n     * Save Tools settings to a map\n     */\n    for (const toolName in this.config.tools) {\n      /**\n       * If Tool is an object not a Tool's class then\n       * save class and settings separately\n       */\n      if (_.isObject(this.config.tools[toolName])) {\n        config[toolName] = this.config.tools[toolName] as ToolSettings;\n      } else {\n        config[toolName] = { class: this.config.tools[toolName] as ToolConstructable };\n      }\n    }\n\n    return config;\n  }\n}\n"
  },
  {
    "path": "src/components/modules/ui.ts",
    "content": "/* eslint-disable jsdoc/no-undefined-types */\n/**\n * Module UI\n *\n * @type {UI}\n */\nimport Module from '../__module';\nimport $, { toggleEmptyMark } from '../dom';\nimport * as _ from '../utils';\n\nimport Selection from '../selection';\nimport Block from '../block';\nimport Flipper from '../flipper';\nimport { mobileScreenBreakpoint } from '../utils';\n\nimport styles from '../../styles/main.css?inline';\nimport { BlockHovered } from '../events/BlockHovered';\nimport { selectionChangeDebounceTimeout } from '../constants';\nimport { EditorMobileLayoutToggled } from '../events';\n/**\n * HTML Elements used for UI\n */\ninterface UINodes {\n  holder: HTMLElement;\n  wrapper: HTMLElement;\n  redactor: HTMLElement;\n}\n\n/**\n * @class\n * @classdesc Makes Editor.js UI:\n *                <codex-editor>\n *                    <ce-redactor />\n *                    <ce-toolbar />\n *                    <ce-inline-toolbar />\n *                </codex-editor>\n * @typedef {UI} UI\n * @property {EditorConfig} config   - editor configuration {@link EditorJS#configuration}\n * @property {object} Editor         - available editor modules {@link EditorJS#moduleInstances}\n * @property {object} nodes          -\n * @property {Element} nodes.holder  - element where we need to append redactor\n * @property {Element} nodes.wrapper  - <codex-editor>\n * @property {Element} nodes.redactor - <ce-redactor>\n */\nexport default class UI extends Module<UINodes> {\n  /**\n   * Editor.js UI CSS class names\n   *\n   * @returns {{editorWrapper: string, editorZone: string}}\n   */\n  public get CSS(): {\n    editorWrapper: string; editorWrapperNarrow: string; editorZone: string; editorZoneHidden: string;\n    editorEmpty: string; editorRtlFix: string;\n    } {\n    return {\n      editorWrapper: 'codex-editor',\n      editorWrapperNarrow: 'codex-editor--narrow',\n      editorZone: 'codex-editor__redactor',\n      editorZoneHidden: 'codex-editor__redactor--hidden',\n      editorEmpty: 'codex-editor--empty',\n      editorRtlFix: 'codex-editor--rtl',\n    };\n  }\n\n  /**\n   * Return Width of center column of Editor\n   *\n   * @returns {DOMRect}\n   */\n  public get contentRect(): DOMRect {\n    if (this.contentRectCache !== null) {\n      return this.contentRectCache;\n    }\n\n    const someBlock = this.nodes.wrapper.querySelector(`.${Block.CSS.content}`);\n\n    /**\n     * When Editor is not ready, there is no Blocks, so return the default value\n     */\n    if (!someBlock) {\n      return {\n        width: 650,\n        left: 0,\n        right: 0,\n      } as DOMRect;\n    }\n\n    this.contentRectCache = someBlock.getBoundingClientRect();\n\n    return this.contentRectCache;\n  }\n\n  /**\n   * Flag that became true on mobile viewport\n   *\n   * @type {boolean}\n   */\n  public isMobile = false;\n\n\n  /**\n   * Cache for center column rectangle info\n   * Invalidates on window resize\n   *\n   * @type {DOMRect}\n   */\n  private contentRectCache: DOMRect | null = null;\n\n  /**\n   * Handle window resize only when it finished\n   *\n   * @type {() => void}\n   */\n  private resizeDebouncer: () => void = _.debounce(() => {\n    this.windowResize();\n  // eslint-disable-next-line @typescript-eslint/no-magic-numbers\n  }, 200);\n\n  /**\n   * Handle selection change to manipulate Inline Toolbar appearance\n   */\n  private selectionChangeDebounced = _.debounce(() => {\n    this.selectionChanged();\n  }, selectionChangeDebounceTimeout);\n\n  /**\n   * Making main interface\n   */\n  public async prepare(): Promise<void> {\n    /**\n     * Detect mobile version\n     */\n    this.setIsMobile();\n\n    /**\n     * Make main UI elements\n     */\n    this.make();\n\n    /**\n     * Load and append CSS\n     */\n    this.loadStyles();\n  }\n\n  /**\n   * Toggle read-only state\n   *\n   * If readOnly is true:\n   *  - removes all listeners from main UI module elements\n   *\n   * if readOnly is false:\n   *  - enables all listeners to UI module elements\n   *\n   * @param {boolean} readOnlyEnabled - \"read only\" state\n   */\n  public toggleReadOnly(readOnlyEnabled: boolean): void {\n    /**\n     * Prepare components based on read-only state\n     */\n    if (!readOnlyEnabled) {\n      /**\n       * Postpone events binding to the next tick to make sure all ui elements are ready\n       */\n      window.requestIdleCallback(() => {\n        /**\n         * Bind events for the UI elements\n         */\n        this.bindReadOnlySensitiveListeners();\n      }, {\n        timeout: 2000,\n      });\n    } else {\n      /**\n       * Unbind all events\n       *\n       */\n      this.unbindReadOnlySensitiveListeners();\n    }\n  }\n\n  /**\n   * Check if Editor is empty and set CSS class to wrapper\n   */\n  public checkEmptiness(): void {\n    const { BlockManager } = this.Editor;\n\n    this.nodes.wrapper.classList.toggle(this.CSS.editorEmpty, BlockManager.isEditorEmpty);\n  }\n\n  /**\n   * Check if one of Toolbar is opened\n   * Used to prevent global keydowns (for example, Enter) conflicts with Enter-on-toolbar\n   *\n   * @returns {boolean}\n   */\n  public get someToolbarOpened(): boolean {\n    const { Toolbar, BlockSettings, InlineToolbar } = this.Editor;\n\n    return Boolean(BlockSettings.opened || InlineToolbar.opened || Toolbar.toolbox.opened);\n  }\n\n  /**\n   * Check for some Flipper-buttons is under focus\n   */\n  public get someFlipperButtonFocused(): boolean {\n    /**\n     * Toolbar has internal module (Toolbox) that has own Flipper,\n     * so we check it manually\n     */\n    if (this.Editor.Toolbar.toolbox.hasFocus()) {\n      return true;\n    }\n\n    /* eslint-disable @typescript-eslint/no-unused-vars, no-unused-vars */\n    return Object.entries(this.Editor).filter(([_moduleName, moduleClass]) => {\n      return moduleClass.flipper instanceof Flipper;\n    })\n      .some(([_moduleName, moduleClass]) => {\n        return moduleClass.flipper.hasFocus();\n      });\n\n    /* eslint-enable @typescript-eslint/no-unused-vars, no-unused-vars */\n  }\n\n  /**\n   * Clean editor`s UI\n   */\n  public destroy(): void {\n    this.nodes.holder.innerHTML = '';\n\n    this.unbindReadOnlyInsensitiveListeners();\n  }\n\n  /**\n   * Close all Editor's toolbars\n   */\n  public closeAllToolbars(): void {\n    const { Toolbar, BlockSettings, InlineToolbar } = this.Editor;\n\n    BlockSettings.close();\n    InlineToolbar.close();\n    Toolbar.toolbox.close();\n  }\n\n  /**\n   * Event listener for 'mousedown' and 'touchstart' events\n   *\n   * @param event - TouchEvent or MouseEvent\n   */\n  private documentTouchedListener = (event: Event): void => {\n    this.documentTouched(event);\n  };\n\n  /**\n   * Check for mobile mode and save the result\n   */\n  private setIsMobile(): void {\n    const isMobile = window.innerWidth < mobileScreenBreakpoint;\n\n    if (isMobile !== this.isMobile) {\n      /**\n       * Dispatch global event\n       */\n      this.eventsDispatcher.emit(EditorMobileLayoutToggled, {\n        isEnabled: this.isMobile,\n      });\n    }\n\n    this.isMobile = isMobile;\n  }\n\n  /**\n   * Makes Editor.js interface\n   */\n  private make(): void {\n    /**\n     * Element where we need to append Editor.js\n     *\n     * @type {Element}\n     */\n    this.nodes.holder = $.getHolder(this.config.holder);\n\n    /**\n     * Create and save main UI elements\n     */\n    this.nodes.wrapper = $.make('div', [\n      this.CSS.editorWrapper,\n      ...(this.isRtl ? [ this.CSS.editorRtlFix ] : []),\n    ]);\n    this.nodes.redactor = $.make('div', this.CSS.editorZone);\n\n    /**\n     * If Editor has injected into the narrow container, enable Narrow Mode\n     *\n     * @todo Forced layout. Get rid of this feature\n     */\n    if (this.nodes.holder.offsetWidth < this.contentRect.width) {\n      this.nodes.wrapper.classList.add(this.CSS.editorWrapperNarrow);\n    }\n\n    /**\n     * Set customizable bottom zone height\n     */\n    this.nodes.redactor.style.paddingBottom = this.config.minHeight + 'px';\n\n    this.nodes.wrapper.appendChild(this.nodes.redactor);\n    this.nodes.holder.appendChild(this.nodes.wrapper);\n\n    this.bindReadOnlyInsensitiveListeners();\n  }\n\n  /**\n   * Appends CSS\n   */\n  private loadStyles(): void {\n    /**\n     * Load CSS\n     */\n    // eslint-disable-next-line @typescript-eslint/no-var-requires\n    const styleTagId = 'editor-js-styles';\n\n    /**\n     * Do not append styles again if they are already on the page\n     */\n    if ($.get(styleTagId)) {\n      return;\n    }\n\n    /**\n     * Make tag\n     */\n    const tag = $.make('style', null, {\n      id: styleTagId,\n      textContent: styles.toString(),\n    });\n\n    /**\n     * If user enabled Content Security Policy, he can pass nonce through the config\n     *\n     * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce\n     */\n    if (this.config.style && !_.isEmpty(this.config.style) && this.config.style.nonce) {\n      tag.setAttribute('nonce', this.config.style.nonce);\n    }\n\n    /**\n     * Append styles at the top of HEAD tag\n     */\n    $.prepend(document.head, tag);\n  }\n\n  /**\n   * Adds listeners that should work both in read-only and read-write modes\n   */\n  private bindReadOnlyInsensitiveListeners(): void {\n    this.listeners.on(document, 'selectionchange', this.selectionChangeDebounced);\n\n    this.listeners.on(window, 'resize', this.resizeDebouncer, {\n      passive: true,\n    });\n\n    this.listeners.on(this.nodes.redactor, 'mousedown', this.documentTouchedListener, {\n      capture: true,\n      passive: true,\n    });\n\n    this.listeners.on(this.nodes.redactor, 'touchstart', this.documentTouchedListener, {\n      capture: true,\n      passive: true,\n    });\n  }\n\n  /**\n   * Removes listeners that should work both in read-only and read-write modes\n   */\n  private unbindReadOnlyInsensitiveListeners(): void {\n    this.listeners.off(document, 'selectionchange', this.selectionChangeDebounced);\n    this.listeners.off(window, 'resize', this.resizeDebouncer);\n    this.listeners.off(this.nodes.redactor, 'mousedown', this.documentTouchedListener);\n    this.listeners.off(this.nodes.redactor, 'touchstart', this.documentTouchedListener);\n  }\n\n\n  /**\n   * Adds listeners that should work only in read-only mode\n   */\n  private bindReadOnlySensitiveListeners(): void {\n    this.readOnlyMutableListeners.on(this.nodes.redactor, 'click', (event: MouseEvent) => {\n      this.redactorClicked(event);\n    }, false);\n\n    this.readOnlyMutableListeners.on(document, 'keydown', (event: KeyboardEvent) => {\n      this.documentKeydown(event);\n    }, true);\n\n    this.readOnlyMutableListeners.on(document, 'mousedown', (event: MouseEvent) => {\n      this.documentClicked(event);\n    }, true);\n\n    /**\n     * Start watching 'block-hovered' events that is used by Toolbar for moving\n     */\n    this.watchBlockHoveredEvents();\n\n    /**\n     * We have custom logic for providing placeholders for contenteditable elements.\n     * To make it work, we need to have data-empty mark on empty inputs.\n     */\n    this.enableInputsEmptyMark();\n  }\n\n\n  /**\n   * Listen redactor mousemove to emit 'block-hovered' event\n   */\n  private watchBlockHoveredEvents(): void {\n    /**\n     * Used to not emit the same block multiple times to the 'block-hovered' event on every mousemove\n     */\n    let blockHoveredEmitted;\n\n    this.readOnlyMutableListeners.on(this.nodes.redactor, 'mousemove', _.throttle((event: MouseEvent | TouchEvent) => {\n      const hoveredBlock = (event.target as Element).closest('.ce-block');\n\n      /**\n       * Do not trigger 'block-hovered' for cross-block selection\n       */\n      if (this.Editor.BlockSelection.anyBlockSelected) {\n        return;\n      }\n\n      if (!hoveredBlock) {\n        return;\n      }\n\n      if (blockHoveredEmitted === hoveredBlock) {\n        return;\n      }\n\n      blockHoveredEmitted = hoveredBlock;\n\n      this.eventsDispatcher.emit(BlockHovered, {\n        block: this.Editor.BlockManager.getBlockByChildNode(hoveredBlock),\n      });\n    // eslint-disable-next-line @typescript-eslint/no-magic-numbers\n    }, 20), {\n      passive: true,\n    });\n  }\n\n  /**\n   * Unbind events that should work only in read-only mode\n   */\n  private unbindReadOnlySensitiveListeners(): void {\n    this.readOnlyMutableListeners.clearAll();\n  }\n\n  /**\n   * Resize window handler\n   */\n  private windowResize(): void {\n    /**\n     * Invalidate content zone size cached, because it may be changed\n     */\n    this.contentRectCache = null;\n\n    /**\n     * Detect mobile version\n     */\n    this.setIsMobile();\n  }\n\n  /**\n   * All keydowns on document\n   *\n   * @param {KeyboardEvent} event - keyboard event\n   */\n  private documentKeydown(event: KeyboardEvent): void {\n    switch (event.keyCode) {\n      case _.keyCodes.ENTER:\n        this.enterPressed(event);\n        break;\n\n      case _.keyCodes.BACKSPACE:\n      case _.keyCodes.DELETE:\n        this.backspacePressed(event);\n        break;\n\n      case _.keyCodes.ESC:\n        this.escapePressed(event);\n        break;\n\n      default:\n        this.defaultBehaviour(event);\n        break;\n    }\n  }\n\n  /**\n   * Ignore all other document's keydown events\n   *\n   * @param {KeyboardEvent} event - keyboard event\n   */\n  private defaultBehaviour(event: KeyboardEvent): void {\n    const { currentBlock } = this.Editor.BlockManager;\n    const keyDownOnEditor = (event.target as HTMLElement).closest(`.${this.CSS.editorWrapper}`);\n    const isMetaKey = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;\n\n    /**\n     * When some block is selected, but the caret is not set inside the editor, treat such keydowns as keydown on selected block.\n     */\n    if (currentBlock !== undefined && keyDownOnEditor === null) {\n      this.Editor.BlockEvents.keydown(event);\n\n      return;\n    }\n\n    /**\n     * Ignore keydowns on editor and meta keys\n     */\n    if (keyDownOnEditor || (currentBlock && isMetaKey)) {\n      return;\n    }\n\n    /**\n     * Remove all highlights and remove caret\n     */\n    this.Editor.BlockManager.unsetCurrentBlock();\n\n    /**\n     * Close Toolbar\n     */\n    this.Editor.Toolbar.close();\n  }\n\n  /**\n   * @param {KeyboardEvent} event - keyboard event\n   */\n  private backspacePressed(event: KeyboardEvent): void {\n    const { BlockManager, BlockSelection, Caret } = this.Editor;\n\n    /**\n     * If any block selected and selection doesn't exists on the page (that means no other editable element is focused),\n     * remove selected blocks\n     */\n    if (BlockSelection.anyBlockSelected && !Selection.isSelectionExists) {\n      const selectionPositionIndex = BlockManager.removeSelectedBlocks();\n\n      const newBlock = BlockManager.insertDefaultBlockAtIndex(selectionPositionIndex, true);\n\n      Caret.setToBlock(newBlock, Caret.positions.START);\n\n      /** Clear selection */\n      BlockSelection.clearSelection(event);\n\n      /**\n       * Stop propagations\n       * Manipulation with BlockSelections is handled in global backspacePress because they may occur\n       * with CMD+A or RectangleSelection and they can be handled on document event\n       */\n      event.preventDefault();\n      event.stopPropagation();\n      event.stopImmediatePropagation();\n    }\n  }\n\n  /**\n   * Escape pressed\n   * If some of Toolbar components are opened, then close it otherwise close Toolbar\n   *\n   * @param {Event} event - escape keydown event\n   */\n  private escapePressed(event): void {\n    /**\n     * Clear blocks selection by ESC\n     */\n    this.Editor.BlockSelection.clearSelection(event);\n\n    if (this.Editor.Toolbar.toolbox.opened) {\n      this.Editor.Toolbar.toolbox.close();\n      this.Editor.Caret.setToBlock(this.Editor.BlockManager.currentBlock, this.Editor.Caret.positions.END);\n    } else if (this.Editor.BlockSettings.opened) {\n      this.Editor.BlockSettings.close();\n    } else if (this.Editor.InlineToolbar.opened) {\n      this.Editor.InlineToolbar.close();\n    } else {\n      this.Editor.Toolbar.close();\n    }\n  }\n\n  /**\n   * Enter pressed on document\n   *\n   * @param {KeyboardEvent} event - keyboard event\n   */\n  private enterPressed(event: KeyboardEvent): void {\n    const { BlockManager, BlockSelection } = this.Editor;\n\n    if (this.someToolbarOpened) {\n      return;\n    }\n\n    const hasPointerToBlock = BlockManager.currentBlockIndex >= 0;\n\n    /**\n     * If any block selected and selection doesn't exists on the page (that means no other editable element is focused),\n     * remove selected blocks\n     */\n    if (BlockSelection.anyBlockSelected && !Selection.isSelectionExists) {\n      /** Clear selection */\n      BlockSelection.clearSelection(event);\n\n      /**\n       * Stop propagations\n       * Manipulation with BlockSelections is handled in global enterPress because they may occur\n       * with CMD+A or RectangleSelection\n       */\n      event.preventDefault();\n      event.stopImmediatePropagation();\n      event.stopPropagation();\n\n      return;\n    }\n\n    /**\n     * If Caret is not set anywhere, event target on Enter is always Element that we handle\n     * In our case it is document.body\n     *\n     * So, BlockManager points some Block and Enter press is on Body\n     * We can create a new block\n     */\n    if (!this.someToolbarOpened && hasPointerToBlock && (event.target as HTMLElement).tagName === 'BODY') {\n      /**\n       * Insert the default typed Block\n       */\n      const newBlock = this.Editor.BlockManager.insert();\n\n      /**\n       * Prevent default enter behaviour to prevent adding a new line (<div><br></div>) to the inserted block\n       */\n      event.preventDefault();\n      this.Editor.Caret.setToBlock(newBlock);\n\n      /**\n       * Move toolbar and show plus button because new Block is empty\n       */\n      this.Editor.Toolbar.moveAndOpen(newBlock);\n    }\n\n    this.Editor.BlockSelection.clearSelection(event);\n  }\n\n  /**\n   * All clicks on document\n   *\n   * @param {MouseEvent} event - Click event\n   */\n  private documentClicked(event: MouseEvent): void {\n    /**\n     * Sometimes we emulate click on some UI elements, for example by Enter on Block Settings button\n     * We don't need to handle such events, because they handled in other place.\n     */\n    if (!event.isTrusted) {\n      return;\n    }\n    /**\n     * Close Inline Toolbar when nothing selected\n     * Do not fire check on clicks at the Inline Toolbar buttons\n     */\n    const target = event.target as HTMLElement;\n    const clickedInsideOfEditor = this.nodes.holder.contains(target) || Selection.isAtEditor;\n\n    if (!clickedInsideOfEditor) {\n      /**\n       * Clear pointer on BlockManager\n       *\n       * Current page might contain several instances\n       * Click between instances MUST clear focus, pointers and close toolbars\n       */\n      this.Editor.BlockManager.unsetCurrentBlock();\n      this.Editor.Toolbar.close();\n    }\n\n    /**\n     * If Block Settings opened, close them by click on document.\n     *\n     * But allow clicking inside Block Settings.\n     * Also, do not process clicks on the Block Settings Toggler, because it has own click listener\n     */\n    const isClickedInsideBlockSettings = this.Editor.BlockSettings.nodes.wrapper?.contains(target);\n    const isClickedInsideBlockSettingsToggler = this.Editor.Toolbar.nodes.settingsToggler?.contains(target);\n    const doNotProcess = isClickedInsideBlockSettings || isClickedInsideBlockSettingsToggler;\n\n    if (this.Editor.BlockSettings.opened && !doNotProcess) {\n      this.Editor.BlockSettings.close();\n\n      const clickedBlock = this.Editor.BlockManager.getBlockByChildNode(target);\n\n      this.Editor.Toolbar.moveAndOpen(clickedBlock);\n    }\n\n    /**\n     * Clear Selection if user clicked somewhere\n     */\n    this.Editor.BlockSelection.clearSelection(event);\n  }\n\n  /**\n   * First touch on editor\n   * Fired before click\n   *\n   * Used to change current block — we need to do it before 'selectionChange' event.\n   * Also:\n   * - Move and show the Toolbar\n   * - Set a Caret\n   *\n   * @param event - touch or mouse event\n   */\n  private documentTouched(event: Event): void {\n    let clickedNode = event.target as HTMLElement;\n\n    /**\n     * If click was fired on Editor`s wrapper, try to get clicked node by elementFromPoint method\n     */\n    if (clickedNode === this.nodes.redactor) {\n      const clientX = event instanceof MouseEvent ? event.clientX : (event as TouchEvent).touches[0].clientX;\n      const clientY = event instanceof MouseEvent ? event.clientY : (event as TouchEvent).touches[0].clientY;\n\n      clickedNode = document.elementFromPoint(clientX, clientY) as HTMLElement;\n    }\n\n    /**\n     * Select clicked Block as Current\n     */\n    try {\n      this.Editor.BlockManager.setCurrentBlockByChildNode(clickedNode);\n    } catch (e) {\n      /**\n       * If clicked outside first-level Blocks and it is not RectSelection, set Caret to the last empty Block\n       */\n      if (!this.Editor.RectangleSelection.isRectActivated()) {\n        this.Editor.Caret.setToTheLastBlock();\n      }\n    }\n\n    /**\n     * Move and open toolbar\n     * (used for showing Block Settings toggler after opening and closing Inline Toolbar)\n     */\n    if (!this.Editor.ReadOnly.isEnabled) {\n      this.Editor.Toolbar.moveAndOpen();\n    }\n  }\n\n  /**\n   * All clicks on the redactor zone\n   *\n   * @param {MouseEvent} event - click event\n   * @description\n   * - By clicks on the Editor's bottom zone:\n   *      - if last Block is empty, set a Caret to this\n   *      - otherwise, add a new empty Block and set a Caret to that\n   */\n  private redactorClicked(event: MouseEvent): void {\n    if (!Selection.isCollapsed) {\n      return;\n    }\n\n    /**\n     * case when user clicks on anchor element\n     * if it is clicked via ctrl key, then we open new window with url\n     */\n    const element = event.target as Element;\n    const ctrlKey = event.metaKey || event.ctrlKey;\n    const anchor = $.getClosestAnchor(element);\n  \n    if (anchor && ctrlKey) {\n      event.stopImmediatePropagation();\n      event.stopPropagation();\n\n      const href = anchor.getAttribute('href');\n      const validUrl = _.getValidUrl(href);\n\n      _.openTab(validUrl);\n\n      return;\n    }\n\n    this.processBottomZoneClick(event);\n  }\n\n  /**\n   * Check if user clicks on the Editor's bottom zone:\n   *  - set caret to the last block\n   *  - or add new empty block\n   *\n   * @param event - click event\n   */\n  private processBottomZoneClick(event: MouseEvent): void {\n    const lastBlock = this.Editor.BlockManager.getBlockByIndex(-1);\n\n    const lastBlockBottomCoord = $.offset(lastBlock.holder).bottom;\n    const clickedCoord = event.pageY;\n    const { BlockSelection } = this.Editor;\n    const isClickedBottom = event.target instanceof Element &&\n      event.target.isEqualNode(this.nodes.redactor) &&\n      /**\n       * If there is cross block selection started, target will be equal to redactor so we need additional check\n       */\n      !BlockSelection.anyBlockSelected &&\n\n      /**\n       * Prevent caret jumping (to last block) when clicking between blocks\n       */\n      lastBlockBottomCoord < clickedCoord;\n\n    if (isClickedBottom) {\n      event.stopImmediatePropagation();\n      event.stopPropagation();\n\n      const { BlockManager, Caret, Toolbar } = this.Editor;\n\n      /**\n       * Insert a default-block at the bottom if:\n       * - last-block is not a default-block (Text)\n       *   to prevent unnecessary tree-walking on Tools with many nodes (for ex. Table)\n       * - Or, default-block is not empty\n       */\n      if (!BlockManager.lastBlock.tool.isDefault || !BlockManager.lastBlock.isEmpty) {\n        BlockManager.insertAtEnd();\n      }\n\n      /**\n       * Set the caret and toolbar to empty Block\n       */\n      Caret.setToTheLastBlock();\n      Toolbar.moveAndOpen(BlockManager.lastBlock);\n    }\n  }\n\n  /**\n   * Handle selection changes on mobile devices\n   * Uses for showing the Inline Toolbar\n   */\n  private selectionChanged(): void {\n    const { CrossBlockSelection, BlockSelection } = this.Editor;\n    const focusedElement = Selection.anchorElement;\n\n    if (CrossBlockSelection.isCrossBlockSelectionStarted) {\n      // Removes all ranges when any Block is selected\n      if (BlockSelection.anyBlockSelected) {\n        Selection.get().removeAllRanges();\n      }\n    }\n\n    /**\n     * Usual clicks on some controls, for example, Block Tunes Toggler\n     */\n    if (!focusedElement) {\n      /**\n       * If there is no selected range, close inline toolbar\n       *\n       * @todo Make this method more straightforward\n       */\n      if (!Selection.range) {\n        this.Editor.InlineToolbar.close();\n      }\n\n      return;\n    }\n\n    /**\n     * Event can be fired on clicks at non-block-content elements,\n     * for example, at the Inline Toolbar or some Block Tune element.\n     * We also make sure that the closest block belongs to the current editor and not a parent\n     */\n    const closestBlock = focusedElement.closest(`.${Block.CSS.content}`);\n    const clickedOutsideBlockContent = closestBlock === null || (closestBlock.closest(`.${Selection.CSS.editorWrapper}`) !== this.nodes.wrapper);\n\n    if (clickedOutsideBlockContent) {\n      /**\n       * If new selection is not on Inline Toolbar, we need to close it\n       */\n      if (!this.Editor.InlineToolbar.containsNode(focusedElement)) {\n        this.Editor.InlineToolbar.close();\n      }\n\n      /**\n       * Case when we click on external tool elements,\n       * for example some Block Tune element.\n       * If this external content editable element has data-inline-toolbar=\"true\"\n       */\n      const inlineToolbarEnabledForExternalTool = (focusedElement as HTMLElement).dataset.inlineToolbar === 'true';\n\n      if (!inlineToolbarEnabledForExternalTool) {\n        return;\n      }\n    }\n\n    /**\n     * Set current block when entering to Editor.js by tab key\n     */\n    if (!this.Editor.BlockManager.currentBlock) {\n      this.Editor.BlockManager.setCurrentBlockByChildNode(focusedElement);\n    }\n\n    this.Editor.InlineToolbar.tryToShow(true);\n  }\n\n  /**\n   * Editor.js provides and ability to show placeholders for empty contenteditable elements\n   *\n   * This method watches for input and focus events and toggles 'data-empty' attribute\n   * to workaroud the case, when inputs contains only <br>s and has no visible content\n   * Then, CSS could rely on this attribute to show placeholders\n   */\n  private enableInputsEmptyMark(): void {\n    /**\n     * Toggle data-empty attribute on input depending on its emptiness\n     *\n     * @param event - input or focus event\n     */\n    function handleInputOrFocusChange(event: Event): void {\n      const input = event.target as HTMLElement;\n\n      toggleEmptyMark(input);\n    }\n\n    this.readOnlyMutableListeners.on(this.nodes.wrapper, 'input', handleInputOrFocusChange);\n    this.readOnlyMutableListeners.on(this.nodes.wrapper, 'focusin', handleInputOrFocusChange);\n    this.readOnlyMutableListeners.on(this.nodes.wrapper, 'focusout', handleInputOrFocusChange);\n  }\n}\n"
  },
  {
    "path": "src/components/polyfills.ts",
    "content": "'use strict';\n\n/**\n * Extend Element interface to include prefixed and experimental properties\n */\ninterface Element {\n  matchesSelector: (selector: string) => boolean;\n  mozMatchesSelector: (selector: string) => boolean;\n  msMatchesSelector: (selector: string) => boolean;\n  oMatchesSelector: (selector: string) => boolean;\n\n  prepend: (...nodes: Array<string | Node>) => void;\n  append: (...nodes: Array<string | Node>) => void;\n}\n\n/**\n * The Element.matches() method returns true if the element\n * would be selected by the specified selector string;\n * otherwise, returns false.\n *\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/matches#Polyfill}\n * @param {string} s - selector\n */\nif (!Element.prototype.matches) {\n  Element.prototype.matches = Element.prototype.matchesSelector ||\n    Element.prototype.mozMatchesSelector ||\n    Element.prototype.msMatchesSelector ||\n    Element.prototype.oMatchesSelector ||\n    Element.prototype.webkitMatchesSelector ||\n    function (s): boolean {\n      const matches = (this.document || this.ownerDocument).querySelectorAll(s);\n      let i = matches.length;\n\n      while (--i >= 0 && matches.item(i) !== this) {\n      }\n\n      return i > -1;\n    };\n}\n\n/**\n * The Element.closest() method returns the closest ancestor\n * of the current element (or the current element itself) which\n * matches the selectors given in parameter.\n * If there isn't such an ancestor, it returns null.\n *\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill}\n * @param {string} s - selector\n */\nif (!Element.prototype.closest) {\n  Element.prototype.closest = function (s): Element | null {\n    // eslint-disable-next-line @typescript-eslint/no-this-alias\n    let el = this;\n\n    if (!document.documentElement.contains(el)) {\n      return null;\n    }\n\n    do {\n      if (el.matches(s)) {\n        return el;\n      }\n\n      el = el.parentElement || el.parentNode;\n    } while (el !== null);\n\n    return null;\n  };\n}\n\n/**\n * The ParentNode.prepend method inserts a set of Node objects\n * or DOMString objects before the first child of the ParentNode.\n * DOMString objects are inserted as equivalent Text nodes.\n *\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/ParentNode/prepend#Polyfill}\n * @param {Node | Node[] | string | string[]} nodes - nodes to prepend\n */\nif (!Element.prototype.prepend) {\n  Element.prototype.prepend = function prepend(nodes: Array<Node | string> | Node | string): void {\n    const docFrag = document.createDocumentFragment();\n\n    if (!Array.isArray(nodes)) {\n      nodes = [ nodes ];\n    }\n\n    nodes.forEach((node: Node | string) => {\n      const isNode = node instanceof Node;\n\n      docFrag.appendChild(isNode ? node as Node : document.createTextNode(node as string));\n    });\n\n    this.insertBefore(docFrag, this.firstChild);\n  };\n}\n\ninterface Element {\n  /**\n   * Scrolls the current element into the visible area of the browser window\n   *\n   * @param centerIfNeeded - true, if the element should be aligned so it is centered within the visible area of the scrollable ancestor.\n   */\n  scrollIntoViewIfNeeded(centerIfNeeded?: boolean): void;\n}\n\n/**\n * ScrollIntoViewIfNeeded polyfill by KilianSSL (forked from hsablonniere)\n *\n * @see {@link https://gist.github.com/KilianSSL/774297b76378566588f02538631c3137}\n * @param centerIfNeeded - true, if the element should be aligned so it is centered within the visible area of the scrollable ancestor.\n */\nif (!Element.prototype.scrollIntoViewIfNeeded) {\n  Element.prototype.scrollIntoViewIfNeeded = function (centerIfNeeded): void {\n    centerIfNeeded = arguments.length === 0 ? true : !!centerIfNeeded;\n\n    const parent = this.parentNode,\n        parentComputedStyle = window.getComputedStyle(parent, null),\n        parentBorderTopWidth = parseInt(parentComputedStyle.getPropertyValue('border-top-width')),\n        parentBorderLeftWidth = parseInt(parentComputedStyle.getPropertyValue('border-left-width')),\n        overTop = this.offsetTop - parent.offsetTop < parent.scrollTop,\n        overBottom = (this.offsetTop - parent.offsetTop + this.clientHeight - parentBorderTopWidth) > (parent.scrollTop + parent.clientHeight),\n        overLeft = this.offsetLeft - parent.offsetLeft < parent.scrollLeft,\n        overRight = (this.offsetLeft - parent.offsetLeft + this.clientWidth - parentBorderLeftWidth) > (parent.scrollLeft + parent.clientWidth),\n        alignWithTop = overTop && !overBottom;\n\n    if ((overTop || overBottom) && centerIfNeeded) {\n      parent.scrollTop = this.offsetTop - parent.offsetTop - parent.clientHeight / 2 - parentBorderTopWidth + this.clientHeight / 2;\n    }\n\n    if ((overLeft || overRight) && centerIfNeeded) {\n      parent.scrollLeft = this.offsetLeft - parent.offsetLeft - parent.clientWidth / 2 - parentBorderLeftWidth + this.clientWidth / 2;\n    }\n\n    if ((overTop || overBottom || overLeft || overRight) && !centerIfNeeded) {\n      this.scrollIntoView(alignWithTop);\n    }\n  };\n}\n\n/**\n * RequestIdleCallback polyfill (shims)\n *\n * @see https://developer.chrome.com/blog/using-requestidlecallback/\n * @param cb - callback to be executed when the browser is idle\n */\nwindow.requestIdleCallback = window.requestIdleCallback || function (cb) {\n  const start = Date.now();\n\n  return setTimeout(function () {\n    cb({\n      didTimeout: false,\n      timeRemaining: function () {\n        // eslint-disable-next-line @typescript-eslint/no-magic-numbers\n        return Math.max(0, 50 - (Date.now() - start));\n      },\n    });\n  }, 1);\n};\n\nwindow.cancelIdleCallback = window.cancelIdleCallback || function (id) {\n  clearTimeout(id);\n};\n"
  },
  {
    "path": "src/components/selection.ts",
    "content": "/**\n * TextRange interface for IE9-\n */\nimport * as _ from './utils';\nimport $ from './dom';\n\ninterface TextRange {\n  boundingTop: number;\n  boundingLeft: number;\n  boundingBottom: number;\n  boundingRight: number;\n  boundingHeight: number;\n  boundingWidth: number;\n}\n\n/**\n * Interface for object returned by document.selection in IE9-\n */\ninterface MSSelection {\n  createRange: () => TextRange;\n  type: string;\n}\n\n/**\n * Extends Document interface for IE9-\n */\ninterface Document {\n  selection?: MSSelection;\n}\n\n/**\n * Working with selection\n *\n * @typedef {SelectionUtils} SelectionUtils\n */\nexport default class SelectionUtils {\n  /**\n   * Selection instances\n   *\n   * @todo Check if this is still relevant\n   */\n  public instance: Selection = null;\n  public selection: Selection = null;\n\n  /**\n   * This property can store SelectionUtils's range for restoring later\n   *\n   * @type {Range|null}\n   */\n  public savedSelectionRange: Range = null;\n\n  /**\n   * Fake background is active\n   *\n   * @returns {boolean}\n   */\n  public isFakeBackgroundEnabled = false;\n\n  /**\n   * Native Document's command for fake background\n   */\n  private readonly commandBackground: string = 'backColor';\n\n  /**\n   * Editor styles\n   *\n   * @returns {{editorWrapper: string, editorZone: string}}\n   */\n  public static get CSS(): { editorWrapper: string; editorZone: string } {\n    return {\n      editorWrapper: 'codex-editor',\n      editorZone: 'codex-editor__redactor',\n    };\n  }\n\n  /**\n   * Returns selected anchor\n   * {@link https://developer.mozilla.org/ru/docs/Web/API/Selection/anchorNode}\n   *\n   * @returns {Node|null}\n   */\n  public static get anchorNode(): Node | null {\n    const selection = window.getSelection();\n\n    return selection ? selection.anchorNode : null;\n  }\n\n  /**\n   * Returns selected anchor element\n   *\n   * @returns {Element|null}\n   */\n  public static get anchorElement(): Element | null {\n    const selection = window.getSelection();\n\n    if (!selection) {\n      return null;\n    }\n\n    const anchorNode = selection.anchorNode;\n\n    if (!anchorNode) {\n      return null;\n    }\n\n    if (!$.isElement(anchorNode)) {\n      return anchorNode.parentElement;\n    } else {\n      return anchorNode;\n    }\n  }\n\n  /**\n   * Returns selection offset according to the anchor node\n   * {@link https://developer.mozilla.org/ru/docs/Web/API/Selection/anchorOffset}\n   *\n   * @returns {number|null}\n   */\n  public static get anchorOffset(): number | null {\n    const selection = window.getSelection();\n\n    return selection ? selection.anchorOffset : null;\n  }\n\n  /**\n   * Is current selection range collapsed\n   *\n   * @returns {boolean|null}\n   */\n  public static get isCollapsed(): boolean | null {\n    const selection = window.getSelection();\n\n    return selection ? selection.isCollapsed : null;\n  }\n\n  /**\n   * Check current selection if it is at Editor's zone\n   *\n   * @returns {boolean}\n   */\n  public static get isAtEditor(): boolean {\n    return this.isSelectionAtEditor(SelectionUtils.get());\n  }\n\n  /**\n   * Check if passed selection is at Editor's zone\n   *\n   * @param selection - Selection object to check\n   */\n  public static isSelectionAtEditor(selection: Selection): boolean {\n    if (!selection) {\n      return false;\n    }\n\n    /**\n     * Something selected on document\n     */\n    let selectedNode = (selection.anchorNode || selection.focusNode) as HTMLElement;\n\n    if (selectedNode && selectedNode.nodeType === Node.TEXT_NODE) {\n      selectedNode = selectedNode.parentNode as HTMLElement;\n    }\n\n    let editorZone = null;\n\n    if (selectedNode && selectedNode instanceof Element) {\n      editorZone = selectedNode.closest(`.${SelectionUtils.CSS.editorZone}`);\n    }\n\n    /**\n     * SelectionUtils is not out of Editor because Editor's wrapper was found\n     */\n    return editorZone ? editorZone.nodeType === Node.ELEMENT_NODE : false;\n  }\n\n  /**\n   * Check if passed range at Editor zone\n   *\n   * @param range - range to check\n   */\n  public static isRangeAtEditor(range: Range): boolean {\n    if (!range) {\n      return;\n    }\n\n    let selectedNode = range.startContainer as HTMLElement;\n\n    if (selectedNode && selectedNode.nodeType === Node.TEXT_NODE) {\n      selectedNode = selectedNode.parentNode as HTMLElement;\n    }\n\n    let editorZone = null;\n\n    if (selectedNode && selectedNode instanceof Element) {\n      editorZone = selectedNode.closest(`.${SelectionUtils.CSS.editorZone}`);\n    }\n\n    /**\n     * SelectionUtils is not out of Editor because Editor's wrapper was found\n     */\n    return editorZone ? editorZone.nodeType === Node.ELEMENT_NODE : false;\n  }\n\n  /**\n   * Methods return boolean that true if selection exists on the page\n   */\n  public static get isSelectionExists(): boolean {\n    const selection = SelectionUtils.get();\n\n    return !!selection.anchorNode;\n  }\n\n  /**\n   * Return first range\n   *\n   * @returns {Range|null}\n   */\n  public static get range(): Range | null {\n    return this.getRangeFromSelection(this.get());\n  }\n\n  /**\n   * Returns range from passed Selection object\n   *\n   * @param selection - Selection object to get Range from\n   */\n  public static getRangeFromSelection(selection: Selection): Range | null {\n    return selection && selection.rangeCount ? selection.getRangeAt(0) : null;\n  }\n\n  /**\n   * Calculates position and size of selected text\n   *\n   * @returns {DOMRect | ClientRect}\n   */\n  public static get rect(): DOMRect | ClientRect {\n    let sel: Selection | MSSelection = (document as Document).selection,\n        range: TextRange | Range;\n\n    let rect = {\n      x: 0,\n      y: 0,\n      width: 0,\n      height: 0,\n    } as DOMRect;\n\n    if (sel && sel.type !== 'Control') {\n      sel = sel as MSSelection;\n      range = sel.createRange() as TextRange;\n      rect.x = range.boundingLeft;\n      rect.y = range.boundingTop;\n      rect.width = range.boundingWidth;\n      rect.height = range.boundingHeight;\n\n      return rect;\n    }\n\n    if (!window.getSelection) {\n      _.log('Method window.getSelection is not supported', 'warn');\n\n      return rect;\n    }\n\n    sel = window.getSelection();\n\n    if (sel.rangeCount === null || isNaN(sel.rangeCount)) {\n      _.log('Method SelectionUtils.rangeCount is not supported', 'warn');\n\n      return rect;\n    }\n\n    if (sel.rangeCount === 0) {\n      return rect;\n    }\n\n    range = sel.getRangeAt(0).cloneRange() as Range;\n\n    if (range.getBoundingClientRect) {\n      rect = range.getBoundingClientRect() as DOMRect;\n    }\n    // Fall back to inserting a temporary element\n    if (rect.x === 0 && rect.y === 0) {\n      const span = document.createElement('span');\n\n      if (span.getBoundingClientRect) {\n        // Ensure span has dimensions and position by\n        // adding a zero-width space character\n        span.appendChild(document.createTextNode('\\u200b'));\n        range.insertNode(span);\n        rect = span.getBoundingClientRect() as DOMRect;\n\n        const spanParent = span.parentNode;\n\n        spanParent.removeChild(span);\n\n        // Glue any broken text nodes back together\n        spanParent.normalize();\n      }\n    }\n\n    return rect;\n  }\n\n  /**\n   * Returns selected text as String\n   *\n   * @returns {string}\n   */\n  public static get text(): string {\n    return window.getSelection ? window.getSelection().toString() : '';\n  }\n\n  /**\n   * Returns window SelectionUtils\n   * {@link https://developer.mozilla.org/ru/docs/Web/API/Window/getSelection}\n   *\n   * @returns {Selection}\n   */\n  public static get(): Selection | null  {\n    return window.getSelection();\n  }\n\n  /**\n   * Set focus to contenteditable or native input element\n   *\n   * @param element - element where to set focus\n   * @param offset - offset of cursor\n   */\n  public static setCursor(element: HTMLElement, offset = 0): DOMRect {\n    const range = document.createRange();\n    const selection = window.getSelection();\n\n    /** if found deepest node is native input */\n    if ($.isNativeInput(element)) {\n      if (!$.canSetCaret(element)) {\n        return;\n      }\n\n      element.focus();\n      element.selectionStart = element.selectionEnd = offset;\n\n      return element.getBoundingClientRect();\n    }\n\n    range.setStart(element, offset);\n    range.setEnd(element, offset);\n\n    selection.removeAllRanges();\n    selection.addRange(range);\n\n    return range.getBoundingClientRect();\n  }\n\n  /**\n   * Check if current range exists and belongs to container\n   *\n   * @param container - where range should be\n   */\n  public static isRangeInsideContainer(container: HTMLElement): boolean {\n    const range = SelectionUtils.range;\n\n    if (range === null) {\n      return false;\n    }\n\n    return container.contains(range.startContainer);\n  }\n\n  /**\n   * Adds fake cursor to the current range\n   */\n  public static addFakeCursor(): void {\n    const range = SelectionUtils.range;\n\n    if (range === null) {\n      return;\n    }\n\n    const fakeCursor = $.make('span', 'codex-editor__fake-cursor');\n\n    fakeCursor.dataset.mutationFree = 'true';\n\n    range.collapse();\n    range.insertNode(fakeCursor);\n  }\n\n  /**\n   * Check if passed element contains a fake cursor\n   *\n   * @param el - where to check\n   */\n  public static isFakeCursorInsideContainer(el: HTMLElement): boolean {\n    return $.find(el, `.codex-editor__fake-cursor`) !== null;\n  }\n\n  /**\n   * Removes fake cursor from a container\n   *\n   * @param container - container to look for\n   */\n  public static removeFakeCursor(container: HTMLElement = document.body): void {\n    const fakeCursor = $.find(container, `.codex-editor__fake-cursor`);\n\n    if (!fakeCursor) {\n      return;\n    }\n\n    fakeCursor.remove();\n  }\n\n  /**\n   * Removes fake background\n   */\n  public removeFakeBackground(): void {\n    if (!this.isFakeBackgroundEnabled) {\n      return;\n    }\n    document.execCommand(this.commandBackground, false, 'transparent');\n\n    this.isFakeBackgroundEnabled = false;\n  }\n\n  /**\n   * Sets fake background\n   */\n  public setFakeBackground(): void {\n    document.execCommand(this.commandBackground, false, '#a8d6ff');\n\n    this.isFakeBackgroundEnabled = true;\n  }\n\n  /**\n   * Save SelectionUtils's range\n   */\n  public save(): void {\n    this.savedSelectionRange = SelectionUtils.range;\n  }\n\n  /**\n   * Restore saved SelectionUtils's range\n   */\n  public restore(): void {\n    if (!this.savedSelectionRange) {\n      return;\n    }\n\n    const sel = window.getSelection();\n\n    sel.removeAllRanges();\n    sel.addRange(this.savedSelectionRange);\n  }\n\n  /**\n   * Clears saved selection\n   */\n  public clearSaved(): void {\n    this.savedSelectionRange = null;\n  }\n\n  /**\n   * Collapse current selection\n   */\n  public collapseToEnd(): void {\n    const sel = window.getSelection();\n    const range = document.createRange();\n\n    range.selectNodeContents(sel.focusNode);\n    range.collapse(false);\n    sel.removeAllRanges();\n    sel.addRange(range);\n  }\n\n  /**\n   * Looks ahead to find passed tag from current selection\n   *\n   * @param  {string} tagName       - tag to found\n   * @param  {string} [className]   - tag's class name\n   * @param  {number} [searchDepth] - count of tags that can be included. For better performance.\n   * @returns {HTMLElement|null}\n   */\n  public findParentTag(tagName: string, className?: string, searchDepth = 10): HTMLElement | null {\n    const selection = window.getSelection();\n    let parentTag = null;\n\n    /**\n     * If selection is missing or no anchorNode or focusNode were found then return null\n     */\n    if (!selection || !selection.anchorNode || !selection.focusNode) {\n      return null;\n    }\n\n    /**\n     * Define Nodes for start and end of selection\n     */\n    const boundNodes = [\n      /** the Node in which the selection begins */\n      selection.anchorNode as HTMLElement,\n      /** the Node in which the selection ends */\n      selection.focusNode as HTMLElement,\n    ];\n\n    /**\n     * For each selection parent Nodes we try to find target tag [with target class name]\n     * It would be saved in parentTag variable\n     */\n    boundNodes.forEach((parent) => {\n      /** Reset tags limit */\n      let searchDepthIterable = searchDepth;\n\n      while (searchDepthIterable > 0 && parent.parentNode) {\n        /**\n         * Check tag's name\n         */\n        if (parent.tagName === tagName) {\n          /**\n           * Save the result\n           */\n          parentTag = parent;\n\n          /**\n           * Optional additional check for class-name mismatching\n           */\n          if (className && parent.classList && !parent.classList.contains(className)) {\n            parentTag = null;\n          }\n\n          /**\n           * If we have found required tag with class then go out from the cycle\n           */\n          if (parentTag) {\n            break;\n          }\n        }\n\n        /**\n         * Target tag was not found. Go up to the parent and check it\n         */\n        parent = parent.parentNode as HTMLElement;\n        searchDepthIterable--;\n      }\n    });\n\n    /**\n     * Return found tag or null\n     */\n    return parentTag;\n  }\n\n  /**\n   * Expands selection range to the passed parent node\n   *\n   * @param {HTMLElement} element - element which contents should be selected\n   */\n  public expandToTag(element: HTMLElement): void {\n    const selection = window.getSelection();\n\n    selection.removeAllRanges();\n    const range = document.createRange();\n\n    range.selectNodeContents(element);\n    selection.addRange(range);\n  }\n}\n"
  },
  {
    "path": "src/components/tools/base.ts",
    "content": "import type { Tool, ToolConstructable, ToolSettings } from '@/types/tools';\nimport type { SanitizerConfig, API as ApiMethods } from '@/types';\nimport * as _ from '../utils';\nimport { ToolType } from '@/types/tools/adapters/tool-type';\nimport type { BaseToolAdapter as BaseToolAdapterInterface } from '@/types/tools/adapters/base-tool-adapter';\nimport type { InlineToolAdapter as InlineToolAdapterInterface } from '@/types/tools/adapters/inline-tool-adapter';\nimport type { BlockToolAdapter as BlockToolAdapterInterface } from '@/types/tools/adapters/block-tool-adapter';\nimport type { BlockTuneAdapter as BlockTuneAdapterInterface } from '@/types/tools/adapters/block-tune-adapter';\n\n/**\n * Enum of Tool options provided by user\n */\nexport enum UserSettings {\n  /**\n   * Shortcut for Tool\n   */\n  Shortcut = 'shortcut',\n  /**\n   * Toolbox config for Tool\n   */\n  Toolbox = 'toolbox',\n  /**\n   * Enabled Inline Tools for Block Tool\n   */\n  EnabledInlineTools = 'inlineToolbar',\n  /**\n   * Enabled Block Tunes for Block Tool\n   */\n  EnabledBlockTunes = 'tunes',\n  /**\n   * Tool configuration\n   */\n  Config = 'config',\n}\n\n/**\n * Enum of Tool options provided by Tool\n */\nexport enum CommonInternalSettings {\n  /**\n   * Shortcut for Tool\n   */\n  Shortcut = 'shortcut',\n  /**\n   * Sanitize configuration for Tool\n   */\n  SanitizeConfig = 'sanitize',\n\n}\n\n/**\n * Enum of Tool options provided by Block Tool\n */\nexport enum InternalBlockToolSettings {\n  /**\n   * Is line breaks enabled for Tool\n   */\n  IsEnabledLineBreaks = 'enableLineBreaks',\n  /**\n   * Tool Toolbox config\n   */\n  Toolbox = 'toolbox',\n  /**\n   * Tool conversion config\n   */\n  ConversionConfig = 'conversionConfig',\n  /**\n   * Is readonly mode supported for Tool\n   */\n  IsReadOnlySupported = 'isReadOnlySupported',\n  /**\n   * Tool paste config\n   */\n  PasteConfig = 'pasteConfig'\n}\n\n/**\n * Enum of Tool options provided by Inline Tool\n */\nexport enum InternalInlineToolSettings {\n  /**\n   * Flag specifies Tool is inline\n   */\n  IsInline = 'isInline',\n  /**\n   * Inline Tool title for toolbar\n   */\n  Title = 'title', // for Inline Tools. Block Tools can pass title along with icon through the 'toolbox' static prop.\n\n  /**\n   * Allows inline tool to be available in read-only mode\n   * Can be used, for example, by comments tool\n   */\n  IsReadOnlySupported = 'isReadOnlySupported',\n}\n\n/**\n * Enum of Tool options provided by Block Tune\n */\nexport enum InternalTuneSettings {\n  /**\n   * Flag specifies Tool is Block Tune\n   */\n  IsTune = 'isTune',\n}\n\nexport type ToolOptions = Omit<ToolSettings, 'class'>;\n\ninterface ConstructorOptions {\n  name: string;\n  constructable: ToolConstructable;\n  config: ToolOptions;\n  api: ApiMethods;\n  isDefault: boolean;\n  isInternal: boolean;\n  defaultPlaceholder?: string | false;\n}\n\n/**\n * Base abstract class for Tools\n */\nexport default abstract class BaseToolAdapter<Type extends ToolType = ToolType, ToolClass extends Tool = Tool> implements BaseToolAdapterInterface<ToolType, Tool> {\n  /**\n   * Tool type: Block, Inline or Tune\n   */\n  public type: Type;\n\n  /**\n   * Tool name specified in EditorJS config\n   */\n  public name: string;\n\n  /**\n   * Flag show is current Tool internal (bundled with EditorJS core) or not\n   */\n  public readonly isInternal: boolean;\n\n  /**\n   * Flag show is current Tool default or not\n   */\n  public readonly isDefault: boolean;\n\n  /**\n   * EditorJS API for current Tool\n   */\n  protected api: ApiMethods;\n\n  /**\n   * Current tool user configuration\n   */\n  protected config: ToolOptions;\n\n  /**\n   * Tool's constructable blueprint\n   */\n  protected constructable: ToolConstructable;\n\n  /**\n   * Default placeholder specified in EditorJS user configuration\n   */\n  protected defaultPlaceholder?: string | false;\n\n  /**\n   * @class\n   * @param {ConstructorOptions} options - Constructor options\n   */\n  constructor({\n    name,\n    constructable,\n    config,\n    api,\n    isDefault,\n    isInternal = false,\n    defaultPlaceholder,\n  }: ConstructorOptions) {\n    this.api = api;\n    this.name = name;\n    this.constructable = constructable;\n    this.config = config;\n    this.isDefault = isDefault;\n    this.isInternal = isInternal;\n    this.defaultPlaceholder = defaultPlaceholder;\n  }\n\n  /**\n   * Returns Tool user configuration\n   */\n  public get settings(): ToolOptions {\n    const config = this.config[UserSettings.Config] || {};\n\n    if (this.isDefault && !('placeholder' in config) && this.defaultPlaceholder) {\n      config.placeholder = this.defaultPlaceholder;\n    }\n\n    return config;\n  }\n\n  /**\n   * Calls Tool's reset method\n   */\n  public reset(): void | Promise<void> {\n    if (_.isFunction(this.constructable.reset)) {\n      return this.constructable.reset();\n    }\n  }\n\n  /**\n   * Calls Tool's prepare method\n   */\n  public prepare(): void | Promise<void> {\n    if (_.isFunction(this.constructable.prepare)) {\n      return this.constructable.prepare({\n        toolName: this.name,\n        config: this.settings,\n      });\n    }\n  }\n\n  /**\n   * Returns shortcut for Tool (internal or specified by user)\n   */\n  public get shortcut(): string | undefined {\n    const toolShortcut = this.constructable[CommonInternalSettings.Shortcut];\n    const userShortcut = this.config[UserSettings.Shortcut];\n\n    return userShortcut || toolShortcut;\n  }\n\n  /**\n   * Returns Tool's sanitizer configuration\n   */\n  public get sanitizeConfig(): SanitizerConfig {\n    return this.constructable[CommonInternalSettings.SanitizeConfig] || {};\n  }\n\n  /**\n   * Returns true if Tools is inline\n   */\n  public isInline(): this is InlineToolAdapterInterface {\n    return this.type === ToolType.Inline;\n  }\n\n  /**\n   * Returns true if Tools is block\n   */\n  public isBlock(): this is BlockToolAdapterInterface {\n    return this.type === ToolType.Block;\n  }\n\n  /**\n   * Returns true if Tools is tune\n   */\n  public isTune(): this is BlockTuneAdapterInterface {\n    return this.type === ToolType.Tune;\n  }\n\n  /**\n   * Constructs new Tool instance from constructable blueprint\n   *\n   * @param args\n   */\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  public abstract create(...args: any[]): ToolClass;\n}\n"
  },
  {
    "path": "src/components/tools/block.ts",
    "content": "import BaseToolAdapter, { InternalBlockToolSettings, UserSettings } from './base';\nimport type {\n  BlockAPI,\n  BlockTool as IBlockTool,\n  BlockToolConstructable,\n  BlockToolData,\n  ConversionConfig,\n  PasteConfig, SanitizerConfig, ToolboxConfig,\n  ToolboxConfigEntry\n} from '@/types';\nimport * as _ from '../utils';\nimport type InlineToolAdapter from './inline';\nimport type BlockTuneAdapter from './tune';\nimport ToolsCollection from './collection';\nimport type { BlockToolAdapter as BlockToolAdapterInterface } from '@/types/tools/adapters/block-tool-adapter';\nimport { ToolType } from '@/types/tools/adapters/tool-type';\n\n/**\n * Class to work with Block tools constructables\n */\nexport default class BlockToolAdapter extends BaseToolAdapter<ToolType.Block, IBlockTool> implements BlockToolAdapterInterface {\n  /**\n   * Tool type — Block\n   */\n  public type: ToolType.Block = ToolType.Block;\n\n  /**\n   * InlineTool collection for current Block Tool\n   */\n  public inlineTools: ToolsCollection<InlineToolAdapter> = new ToolsCollection<InlineToolAdapter>();\n\n  /**\n   * BlockTune collection for current Block Tool\n   */\n  public tunes: ToolsCollection<BlockTuneAdapter> = new ToolsCollection<BlockTuneAdapter>();\n\n  /**\n   * Tool's constructable blueprint\n   */\n  protected constructable: BlockToolConstructable;\n\n  /**\n   * Creates new Tool instance\n   *\n   * @param data - Tool data\n   * @param block - BlockAPI for current Block\n   * @param readOnly - True if Editor is in read-only mode\n   */\n  public create(data: BlockToolData, block: BlockAPI, readOnly: boolean): IBlockTool {\n    // eslint-disable-next-line new-cap\n    return new this.constructable({\n      data,\n      block,\n      readOnly,\n      api: this.api,\n      config: this.settings,\n    }) as IBlockTool;\n  }\n\n  /**\n   * Returns true if read-only mode is supported by Tool\n   */\n  public get isReadOnlySupported(): boolean {\n    return this.constructable[InternalBlockToolSettings.IsReadOnlySupported] === true;\n  }\n\n  /**\n   * Returns true if Tool supports linebreaks\n   */\n  public get isLineBreaksEnabled(): boolean {\n    return this.constructable[InternalBlockToolSettings.IsEnabledLineBreaks];\n  }\n\n  /**\n   * Returns Tool toolbox configuration (internal or user-specified).\n   *\n   * Merges internal and user-defined toolbox configs based on the following rules:\n   *\n   * - If both internal and user-defined toolbox configs are arrays their items are merged.\n   * Length of the second one is kept.\n   *\n   * - If both are objects their properties are merged.\n   *\n   * - If one is an object and another is an array than internal config is replaced with user-defined\n   * config. This is made to allow user to override default tool's toolbox representation (single/multiple entries)\n   */\n  public get toolbox(): ToolboxConfigEntry[] | undefined {\n    const toolToolboxSettings = this.constructable[InternalBlockToolSettings.Toolbox] as ToolboxConfig;\n    const userToolboxSettings = this.config[UserSettings.Toolbox];\n\n    if (_.isEmpty(toolToolboxSettings)) {\n      return;\n    }\n    if (userToolboxSettings === false) {\n      return;\n    }\n    /**\n     * Return tool's toolbox settings if user settings are not defined\n     */\n    if (!userToolboxSettings) {\n      return Array.isArray(toolToolboxSettings) ? toolToolboxSettings : [ toolToolboxSettings ];\n    }\n\n    /**\n     * Otherwise merge user settings with tool's settings\n     */\n    if (Array.isArray(toolToolboxSettings)) {\n      if (Array.isArray(userToolboxSettings)) {\n        return userToolboxSettings.map((item, i) => {\n          const toolToolboxEntry = toolToolboxSettings[i];\n\n          if (toolToolboxEntry) {\n            return {\n              ...toolToolboxEntry,\n              ...item,\n            };\n          }\n\n          return item;\n        });\n      }\n\n      return [ userToolboxSettings ];\n    } else {\n      if (Array.isArray(userToolboxSettings)) {\n        return userToolboxSettings;\n      }\n\n      return [\n        {\n          ...toolToolboxSettings,\n          ...userToolboxSettings,\n        },\n      ];\n    }\n  }\n\n  /**\n   * Returns Tool conversion configuration\n   */\n  public get conversionConfig(): ConversionConfig | undefined {\n    return this.constructable[InternalBlockToolSettings.ConversionConfig];\n  }\n\n  /**\n   * Returns enabled inline tools for Tool\n   */\n  public get enabledInlineTools(): boolean | string[] {\n    return this.config[UserSettings.EnabledInlineTools] || false;\n  }\n\n  /**\n   * Returns enabled tunes for Tool\n   */\n  public get enabledBlockTunes(): boolean | string[] {\n    return this.config[UserSettings.EnabledBlockTunes];\n  }\n\n  /**\n   * Returns Tool paste configuration\n   */\n  public get pasteConfig(): PasteConfig {\n    return this.constructable[InternalBlockToolSettings.PasteConfig] ?? {};\n  }\n\n  /**\n   * Returns sanitize configuration for Block Tool including configs from related Inline Tools and Block Tunes\n   */\n  @_.cacheable\n  public get sanitizeConfig(): SanitizerConfig {\n    const toolRules = super.sanitizeConfig;\n    const baseConfig = this.baseSanitizeConfig;\n\n    if (_.isEmpty(toolRules)) {\n      return baseConfig;\n    }\n\n    const toolConfig = {} as SanitizerConfig;\n\n    for (const fieldName in toolRules) {\n      if (Object.prototype.hasOwnProperty.call(toolRules, fieldName)) {\n        const rule = toolRules[fieldName];\n\n        /**\n         * If rule is object, merge it with Inline Tools configuration\n         *\n         * Otherwise pass as it is\n         */\n        if (_.isObject(rule)) {\n          toolConfig[fieldName] = Object.assign({}, baseConfig, rule);\n        } else {\n          toolConfig[fieldName] = rule;\n        }\n      }\n    }\n\n    return toolConfig;\n  }\n\n  /**\n   * Returns sanitizer configuration composed from sanitize config of Inline Tools enabled for Tool\n   */\n  @_.cacheable\n  public get baseSanitizeConfig(): SanitizerConfig {\n    const baseConfig = {};\n\n    Array\n      .from(this.inlineTools.values())\n      .forEach(tool => Object.assign(baseConfig, tool.sanitizeConfig));\n\n    Array\n      .from(this.tunes.values())\n      .forEach(tune => Object.assign(baseConfig, tune.sanitizeConfig));\n\n    return baseConfig;\n  }\n}\n"
  },
  {
    "path": "src/components/tools/collection.ts",
    "content": "import type BlockToolAdapter from './block';\nimport type InlineToolAdapter from './inline';\nimport type BlockTuneAdapter from './tune';\nimport type { ToolsCollection as ToolsCollectionInterface } from '@/types/tools/adapters/tools-collection';\n\n\nexport type ToolClass = BlockToolAdapter | InlineToolAdapter | BlockTuneAdapter;\n\n/**\n * Class to store Editor Tools\n */\nexport default class ToolsCollection<V extends ToolClass = ToolClass> extends Map<string, V> implements ToolsCollectionInterface<V> {\n  /**\n   * Returns Block Tools collection\n   */\n  public get blockTools(): ToolsCollection<BlockToolAdapter> {\n    const tools = Array\n      .from(this.entries())\n      .filter(([, tool]) => tool.isBlock()) as [string, BlockToolAdapter][];\n\n    return new ToolsCollection<BlockToolAdapter>(tools);\n  }\n\n  /**\n   * Returns Inline Tools collection\n   */\n  public get inlineTools(): ToolsCollection<InlineToolAdapter> {\n    const tools = Array\n      .from(this.entries())\n      .filter(([, tool]) => tool.isInline()) as [string, InlineToolAdapter][];\n\n    return new ToolsCollection<InlineToolAdapter>(tools);\n  }\n\n  /**\n   * Returns Block Tunes collection\n   */\n  public get blockTunes(): ToolsCollection<BlockTuneAdapter> {\n    const tools = Array\n      .from(this.entries())\n      .filter(([, tool]) => tool.isTune()) as [string, BlockTuneAdapter][];\n\n    return new ToolsCollection<BlockTuneAdapter>(tools);\n  }\n\n  /**\n   * Returns internal Tools collection\n   */\n  public get internalTools(): ToolsCollection<V> {\n    const tools = Array\n      .from(this.entries())\n      .filter(([, tool]) => tool.isInternal);\n\n    return new ToolsCollection<V>(tools);\n  }\n\n  /**\n   * Returns Tools collection provided by user\n   */\n  public get externalTools(): ToolsCollection<V> {\n    const tools = Array\n      .from(this.entries())\n      .filter(([, tool]) => !tool.isInternal);\n\n    return new ToolsCollection<V>(tools);\n  }\n}\n"
  },
  {
    "path": "src/components/tools/factory.ts",
    "content": "import type { ToolConstructable, ToolSettings } from '../../../types/tools';\nimport { InternalInlineToolSettings, InternalTuneSettings } from './base';\nimport InlineToolAdapter from './inline';\nimport BlockTuneAdapter from './tune';\nimport BlockToolAdapter from './block';\nimport type ApiModule from '../modules/api';\nimport type { EditorConfig } from '../../../types/configs';\n\ntype ToolConstructor = typeof InlineToolAdapter | typeof BlockToolAdapter | typeof BlockTuneAdapter;\n\n/**\n * Factory to construct classes to work with tools\n */\nexport default class ToolsFactory {\n  /**\n   * Tools configuration specified by user\n   */\n  private config: {[name: string]: ToolSettings & { isInternal?: boolean }};\n\n  /**\n   * EditorJS API Module\n   */\n  private api: ApiModule;\n\n  /**\n   * EditorJS configuration\n   */\n  private editorConfig: EditorConfig;\n\n  /**\n   * @class\n   * @param config - tools config\n   * @param editorConfig - EditorJS config\n   * @param api - EditorJS API module\n   */\n  constructor(\n    config: {[name: string]: ToolSettings & { isInternal?: boolean }},\n    editorConfig: EditorConfig,\n    api: ApiModule\n  ) {\n    this.api = api;\n    this.config = config;\n    this.editorConfig = editorConfig;\n  }\n\n  /**\n   * Returns Tool object based on it's type\n   *\n   * @param name - tool name\n   */\n  public get(name: string): InlineToolAdapter | BlockToolAdapter | BlockTuneAdapter {\n    const { class: constructable, isInternal = false, ...config } = this.config[name];\n\n    const Constructor = this.getConstructor(constructable);\n    const isTune = constructable[InternalTuneSettings.IsTune];\n\n    return new Constructor({\n      name,\n      constructable,\n      config,\n      api: this.api.getMethodsForTool(name, isTune),\n      isDefault: name === this.editorConfig.defaultBlock,\n      defaultPlaceholder: this.editorConfig.placeholder,\n      isInternal,\n    });\n  }\n\n  /**\n   * Find appropriate Tool object constructor for Tool constructable\n   *\n   * @param constructable - Tools constructable\n   */\n  private getConstructor(constructable: ToolConstructable): ToolConstructor {\n    switch (true) {\n      case constructable[InternalInlineToolSettings.IsInline]:\n        return InlineToolAdapter;\n      case constructable[InternalTuneSettings.IsTune]:\n        return BlockTuneAdapter;\n      default:\n        return BlockToolAdapter;\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/tools/inline.ts",
    "content": "import BaseToolAdapter, { InternalInlineToolSettings } from './base';\nimport type { InlineTool as IInlineTool, InlineToolConstructable } from '@/types';\nimport type { InlineToolAdapter as InlineToolAdapterInterface } from '@/types/tools/adapters/inline-tool-adapter';\nimport { ToolType } from '@/types/tools/adapters/tool-type';\n\n/**\n * InlineTool object to work with Inline Tools constructables\n */\nexport default class InlineToolAdapter extends BaseToolAdapter<ToolType.Inline, IInlineTool> implements InlineToolAdapterInterface {\n  /**\n   * Tool type — Inline\n   */\n  public type: ToolType.Inline = ToolType.Inline;\n\n  /**\n   * Tool's constructable blueprint\n   */\n  protected constructable: InlineToolConstructable;\n\n  /**\n   * Returns title for Inline Tool if specified by user\n   */\n  public get title(): string {\n    return this.constructable[InternalInlineToolSettings.Title];\n  }\n\n  /**\n   * Constructs new InlineTool instance from constructable\n   */\n  public create(): IInlineTool {\n    // eslint-disable-next-line new-cap\n    return new this.constructable({\n      api: this.api,\n      config: this.settings,\n    }) as IInlineTool;\n  }\n\n  /**\n   * Allows inline tool to be available in read-only mode\n   * Can be used, for example, by comments tool\n   */\n  public get isReadOnlySupported(): boolean {\n    return this.constructable[InternalInlineToolSettings.IsReadOnlySupported] ?? false;\n  }\n}\n"
  },
  {
    "path": "src/components/tools/tune.ts",
    "content": "import BaseToolAdapter from './base';\nimport type { BlockAPI, BlockTune as IBlockTune, BlockTuneConstructable } from '@/types';\nimport type { BlockTuneData } from '@/types/block-tunes/block-tune-data';\nimport type { BlockTuneAdapter as BlockTuneAdapterInterface } from '@/types/tools/adapters/block-tune-adapter';\nimport { ToolType } from '@/types/tools/adapters/tool-type';\n\n/**\n * Stub class for BlockTunes\n *\n * @todo Implement\n */\nexport default class BlockTuneAdapter extends BaseToolAdapter<ToolType.Tune, IBlockTune> implements BlockTuneAdapterInterface {\n  /**\n   * Tool type — Tune\n   */\n  public type: ToolType.Tune = ToolType.Tune;\n\n  /**\n   * Tool's constructable blueprint\n   */\n  protected readonly constructable: BlockTuneConstructable;\n\n  /**\n   * Constructs new BlockTune instance from constructable\n   *\n   * @param data - Tune data\n   * @param block - Block API object\n   */\n  public create(data: BlockTuneData, block: BlockAPI): IBlockTune {\n    // eslint-disable-next-line new-cap\n    return new this.constructable({\n      api: this.api,\n      config: this.settings,\n      block,\n      data,\n    });\n  }\n}\n"
  },
  {
    "path": "src/components/ui/toolbox.ts",
    "content": "import * as _ from '../utils';\nimport { BlockToolAPI } from '../block';\nimport Shortcuts from '../utils/shortcuts';\nimport type BlockToolAdapter from '../tools/block';\nimport type ToolsCollection from '../tools/collection';\nimport type { API, BlockToolData, ToolboxConfigEntry, PopoverItemParams, BlockAPI } from '@/types';\nimport EventsDispatcher from '../utils/events';\nimport I18n from '../i18n';\nimport { I18nInternalNS } from '../i18n/namespace-internal';\nimport { PopoverEvent } from '@/types/utils/popover/popover-event';\nimport Listeners from '../utils/listeners';\nimport Dom from '../dom';\nimport type { Popover } from '../utils/popover';\nimport { PopoverDesktop, PopoverMobile } from '../utils/popover';\nimport { EditorMobileLayoutToggled } from '../events';\n\n/**\n * @todo the first Tab on the Block — focus Plus Button, the second — focus Block Tunes Toggler, the third — focus next Block\n */\n\n/**\n * Event that can be triggered by the Toolbox\n */\nexport enum ToolboxEvent {\n  /**\n   * When the Toolbox is opened\n   */\n  Opened = 'toolbox-opened',\n\n  /**\n   * When the Toolbox is closed\n   */\n  Closed = 'toolbox-closed',\n\n  /**\n   * When the new Block added by Toolbox\n   */\n  BlockAdded = 'toolbox-block-added',\n}\n\n/**\n * Events fired by the Toolbox\n *\n * Event name -> payload\n */\nexport interface ToolboxEventMap {\n  [ToolboxEvent.Opened]: undefined;\n  [ToolboxEvent.Closed]: undefined;\n  [ToolboxEvent.BlockAdded]: {\n    block: BlockAPI\n  };\n}\n\n/**\n * Available i18n dict keys that should be passed to the constructor\n */\ntype ToolboxTextLabelsKeys = 'filter' | 'nothingFound';\n\n/**\n * Toolbox\n * This UI element contains list of Block Tools available to be inserted\n * It appears after click on the Plus Button\n *\n * @implements {EventsDispatcher} with some events, see {@link ToolboxEvent}\n */\nexport default class Toolbox extends EventsDispatcher<ToolboxEventMap> {\n  /**\n   * Returns True if Toolbox is Empty and nothing to show\n   *\n   * @returns {boolean}\n   */\n  public get isEmpty(): boolean {\n    return this.toolsToBeDisplayed.length === 0;\n  }\n\n  /**\n   * Opening state\n   *\n   * @type {boolean}\n   */\n  public opened = false;\n\n  /**\n   * Listeners util instance\n   */\n  protected listeners: Listeners = new Listeners();\n\n  /**\n   * Editor API\n   */\n  private api: API;\n\n  /**\n   * Popover instance. There is a util for vertical lists.\n   * Null until initialized\n   */\n  private popover: Popover | null = null;\n\n  /**\n   * List of Tools available. Some of them will be shown in the Toolbox\n   */\n  private tools: ToolsCollection<BlockToolAdapter>;\n\n  /**\n   * Text labels used in the Toolbox. Should be passed from the i18n module\n   */\n  private i18nLabels: Record<ToolboxTextLabelsKeys, string>;\n\n  /**\n   * Current module HTML Elements\n   */\n  private nodes: {\n    toolbox: HTMLElement;\n  } ;\n\n  /**\n   * CSS styles\n   */\n  private static get CSS(): {\n    toolbox: string;\n    } {\n    return {\n      toolbox: 'ce-toolbox',\n    };\n  }\n\n  /**\n   * Toolbox constructor\n   *\n   * @param options - available parameters\n   * @param options.api - Editor API methods\n   * @param options.tools - Tools available to check whether some of them should be displayed at the Toolbox or not\n   */\n  constructor({ api, tools, i18nLabels }: {api: API; tools: ToolsCollection<BlockToolAdapter>; i18nLabels: Record<ToolboxTextLabelsKeys, string>}) {\n    super();\n\n    this.api = api;\n    this.tools = tools;\n    this.i18nLabels = i18nLabels;\n\n    this.enableShortcuts();\n\n    this.nodes = {\n      toolbox: Dom.make('div', Toolbox.CSS.toolbox),\n    };\n\n    this.initPopover();\n\n    if (import.meta.env.MODE === 'test') {\n      this.nodes.toolbox.setAttribute('data-cy', 'toolbox');\n    }\n\n    this.api.events.on(EditorMobileLayoutToggled, this.handleMobileLayoutToggle);\n  }\n\n  /**\n   * Returns root block settings element\n   */\n  public getElement(): HTMLElement | null {\n    return this.nodes.toolbox;\n  }\n\n  /**\n   * Returns true if the Toolbox has the Flipper activated and the Flipper has selected button\n   */\n  public hasFocus(): boolean | undefined {\n    if (this.popover === null) {\n      return;\n    }\n\n    return 'hasFocus' in this.popover ? this.popover.hasFocus() : undefined;\n  }\n\n  /**\n   * Destroy Module\n   */\n  public destroy(): void {\n    super.destroy();\n\n    if (this.nodes && this.nodes.toolbox) {\n      this.nodes.toolbox.remove();\n    }\n\n    this.removeAllShortcuts();\n    this.popover?.off(PopoverEvent.Closed, this.onPopoverClose);\n    this.listeners.destroy();\n    this.api.events.off(EditorMobileLayoutToggled, this.handleMobileLayoutToggle);\n  }\n\n  /**\n   * Toolbox Tool's button click handler\n   *\n   * @param toolName - tool type to be activated\n   * @param blockDataOverrides - Block data predefined by the activated Toolbox item\n   */\n  public toolButtonActivated(toolName: string, blockDataOverrides: BlockToolData): void {\n    this.insertNewBlock(toolName, blockDataOverrides);\n  }\n\n  /**\n   * Open Toolbox with Tools\n   */\n  public open(): void {\n    if (this.isEmpty) {\n      return;\n    }\n\n    this.popover?.show();\n    this.opened = true;\n    this.emit(ToolboxEvent.Opened);\n  }\n\n  /**\n   * Close Toolbox\n   */\n  public close(): void {\n    this.popover?.hide();\n    this.opened = false;\n    this.emit(ToolboxEvent.Closed);\n  }\n\n  /**\n   * Close Toolbox\n   */\n  public toggle(): void {\n    if (!this.opened) {\n      this.open();\n    } else {\n      this.close();\n    }\n  }\n\n  /**\n   * Destroys existing popover instance and contructs the new one.\n   */\n  public handleMobileLayoutToggle = (): void  => {\n    this.destroyPopover();\n    this.initPopover();\n  };\n\n  /**\n   * Creates toolbox popover and appends it inside wrapper element\n   */\n  private initPopover(): void {\n    const PopoverClass = _.isMobileScreen() ? PopoverMobile : PopoverDesktop;\n\n    this.popover = new PopoverClass({\n      scopeElement: this.api.ui.nodes.redactor,\n      searchable: true,\n      messages: {\n        nothingFound: this.i18nLabels.nothingFound,\n        search: this.i18nLabels.filter,\n      },\n      items: this.toolboxItemsToBeDisplayed,\n    });\n\n    this.popover.on(PopoverEvent.Closed, this.onPopoverClose);\n    this.nodes.toolbox?.append(this.popover.getElement());\n  }\n\n  /**\n   * Destroys popover instance and removes it from DOM\n   */\n  private destroyPopover(): void {\n    if (this.popover !== null) {\n      this.popover.hide();\n      this.popover.off(PopoverEvent.Closed, this.onPopoverClose);\n      this.popover.destroy();\n      this.popover = null;\n    }\n\n    if (this.nodes.toolbox !== null) {\n      this.nodes.toolbox.innerHTML = '';\n    }\n  }\n\n  /**\n   * Handles popover close event\n   */\n  private onPopoverClose = (): void => {\n    this.opened = false;\n    this.emit(ToolboxEvent.Closed);\n  };\n\n  /**\n   * Returns list of tools that enables the Toolbox (by specifying the 'toolbox' getter)\n   */\n  @_.cacheable\n  private get toolsToBeDisplayed(): BlockToolAdapter[] {\n    const result: BlockToolAdapter[] = [];\n\n    this.tools.forEach((tool) => {\n      const toolToolboxSettings = tool.toolbox;\n\n      if (toolToolboxSettings) {\n        result.push(tool);\n      }\n    });\n\n    return result;\n  }\n\n  /**\n   * Returns list of items that will be displayed in toolbox\n   */\n  @_.cacheable\n  private get toolboxItemsToBeDisplayed(): PopoverItemParams[] {\n    /**\n     * Maps tool data to popover item structure\n     */\n    const toPopoverItem = (toolboxItem: ToolboxConfigEntry, tool: BlockToolAdapter, displaySecondaryLabel = true): PopoverItemParams => {\n      return {\n        icon: toolboxItem.icon,\n        title: I18n.t(I18nInternalNS.toolNames, toolboxItem.title || _.capitalize(tool.name)),\n        name: tool.name,\n        onActivate: (): void => {\n          this.toolButtonActivated(tool.name, toolboxItem.data);\n        },\n        secondaryLabel: (tool.shortcut && displaySecondaryLabel) ? _.beautifyShortcut(tool.shortcut) : '',\n      };\n    };\n\n    return this.toolsToBeDisplayed\n      .reduce<PopoverItemParams[]>((result, tool) => {\n        if (Array.isArray(tool.toolbox)) {\n          tool.toolbox.forEach((item, index) => {\n            result.push(toPopoverItem(item, tool, index === 0));\n          });\n        } else if (tool.toolbox !== undefined)  {\n          result.push(toPopoverItem(tool.toolbox, tool));\n        }\n\n        return result;\n      }, []);\n  }\n\n  /**\n   * Iterate all tools and enable theirs shortcuts if specified\n   */\n  private enableShortcuts(): void {\n    this.toolsToBeDisplayed.forEach((tool: BlockToolAdapter) => {\n      const shortcut = tool.shortcut;\n\n      if (shortcut) {\n        this.enableShortcutForTool(tool.name, shortcut);\n      }\n    });\n  }\n\n  /**\n   * Enable shortcut Block Tool implemented shortcut\n   *\n   * @param {string} toolName - Tool name\n   * @param {string} shortcut - shortcut according to the ShortcutData Module format\n   */\n  private enableShortcutForTool(toolName: string, shortcut: string): void {\n    Shortcuts.add({\n      name: shortcut,\n      on: this.api.ui.nodes.redactor,\n      handler: async (event: KeyboardEvent) => {\n        event.preventDefault();\n\n        const currentBlockIndex = this.api.blocks.getCurrentBlockIndex();\n        const currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex);\n\n        /**\n         * Try to convert current Block to shortcut's tool\n         * If conversion is not possible, insert a new Block below\n         */\n        if (currentBlock) {\n          try {\n            const newBlock = await this.api.blocks.convert(currentBlock.id, toolName);\n\n            this.api.caret.setToBlock(newBlock, 'end');\n\n            return;\n          } catch (error) {}\n        }\n\n        this.insertNewBlock(toolName);\n      },\n    });\n  }\n\n  /**\n   * Removes all added shortcuts\n   * Fired when the Read-Only mode is activated\n   */\n  private removeAllShortcuts(): void {\n    this.toolsToBeDisplayed.forEach((tool: BlockToolAdapter) => {\n      const shortcut = tool.shortcut;\n\n      if (shortcut) {\n        Shortcuts.remove(this.api.ui.nodes.redactor, shortcut);\n      }\n    });\n  }\n\n  /**\n   * Inserts new block\n   * Can be called when button clicked on Toolbox or by ShortcutData\n   *\n   * @param {string} toolName - Tool name\n   * @param blockDataOverrides - predefined Block data\n   */\n  private async insertNewBlock(toolName: string, blockDataOverrides?: BlockToolData): Promise<void> {\n    const currentBlockIndex = this.api.blocks.getCurrentBlockIndex();\n    const currentBlock = this.api.blocks.getBlockByIndex(currentBlockIndex);\n\n    if (!currentBlock) {\n      return;\n    }\n\n    /**\n     * On mobile version, we see the Plus Button even near non-empty blocks,\n     * so if current block is not empty, add the new block below the current\n     */\n    const index = currentBlock.isEmpty ? currentBlockIndex : currentBlockIndex + 1;\n\n    let blockData;\n\n    if (blockDataOverrides) {\n      /**\n       * Merge real tool's data with data overrides\n       */\n      const defaultBlockData = await this.api.blocks.composeBlockData(toolName);\n\n      blockData = Object.assign(defaultBlockData, blockDataOverrides);\n    }\n\n    const newBlock = this.api.blocks.insert(\n      toolName,\n      blockData,\n      undefined,\n      index,\n      undefined,\n      currentBlock.isEmpty\n    );\n\n    /**\n     * Apply callback before inserting html\n     */\n    newBlock.call(BlockToolAPI.APPEND_CALLBACK);\n\n    this.api.caret.setToBlock(index);\n\n    this.emit(ToolboxEvent.BlockAdded, {\n      block: newBlock,\n    });\n\n    /**\n     * close toolbar when node is changed\n     */\n    this.api.toolbar.close();\n  }\n}\n"
  },
  {
    "path": "src/components/utils/api.ts",
    "content": "import type { BlockAPI } from '../../../types/api/block';\nimport type { EditorModules } from '../../types-internal/editor-modules';\nimport type Block from '../block';\n\n/**\n * Returns Block instance by passed Block index or Block id\n *\n * @param attribute - either BlockAPI or Block id or Block index\n * @param editor - Editor instance\n */\nexport function resolveBlock(attribute: BlockAPI | BlockAPI['id'] | number, editor: EditorModules): Block | undefined {\n  if (typeof attribute === 'number') {\n    return editor.BlockManager.getBlockByIndex(attribute);\n  }\n\n  if (typeof attribute === 'string') {\n    return editor.BlockManager.getBlockById(attribute);\n  }\n\n  return editor.BlockManager.getBlockById(attribute.id);\n}\n"
  },
  {
    "path": "src/components/utils/bem.ts",
    "content": "const ELEMENT_DELIMITER = '__';\nconst MODIFIER_DELIMITER = '--';\n\n/**\n * Utility function that allows to construct class names from block and element names\n *\n * @example bem('ce-popover)() -> 'ce-popover'\n * @example bem('ce-popover)('container') -> 'ce-popover__container'\n * @example bem('ce-popover)('container', 'hidden') -> 'ce-popover__container--hidden'\n * @example bem('ce-popover)(null, 'hidden') -> 'ce-popover--hidden'\n * @param blockName - string with block name\n */\nexport function bem(blockName: string) {\n  /**\n   * @param elementName - string with element name\n   * @param modifier - modifier to be appended\n   */\n  return (elementName?: string | null, modifier?: string) => {\n    const className = [blockName, elementName]\n      .filter(x => !!x)\n      .join(ELEMENT_DELIMITER);\n\n    return [className, modifier]\n      .filter(x => !!x)\n      .join(MODIFIER_DELIMITER);\n  };\n}\n"
  },
  {
    "path": "src/components/utils/blocks.ts",
    "content": "import type { BlockAPI, ToolConfig } from '../../../types';\nimport type { ConversionConfig } from '../../../types/configs/conversion-config';\nimport type { SavedData } from '../../../types/data-formats';\nimport type { BlockToolData } from '../../../types/tools/block-tool-data';\nimport type Block from '../block';\nimport type BlockToolAdapter from '../tools/block';\nimport { isFunction, isString, log, equals, isEmpty } from '../utils';\nimport { isToolConvertable } from './tools';\n\n\n/**\n * Check if block has valid conversion config for export or import.\n *\n * @param block - block to check\n * @param direction - export for block to merge from, import for block to merge to\n */\nexport function isBlockConvertable(block: Block, direction: 'export' | 'import'): boolean {\n  return isToolConvertable(block.tool, direction);\n}\n\n/**\n * Checks that all the properties of the first block data exist in second block data with the same values.\n *\n * Example:\n *\n * data1 = { level: 1 }\n *\n * data2 = {\n *    text: \"Heading text\",\n *    level: 1\n *  }\n *\n * isSameBlockData(data1, data2) => true\n *\n * @param data1 – first block data\n * @param data2 – second block data\n */\nexport function isSameBlockData(data1: BlockToolData, data2: BlockToolData): boolean {\n  return Object.entries(data1).some((([propName, propValue]) => {\n    return data2[propName] && equals(data2[propName], propValue);\n  }));\n}\n\n/**\n * Returns list of tools you can convert specified block to\n *\n * @param block - block to get conversion items for\n * @param allBlockTools - all block tools available in the editor\n */\nexport async function getConvertibleToolsForBlock(block: BlockAPI, allBlockTools: BlockToolAdapter[]): Promise<BlockToolAdapter[]> {\n  const savedData = await block.save() as SavedData;\n  const blockData = savedData.data;\n\n  /**\n   * Checking that the block's tool has an «export» rule\n   */\n  const blockTool = allBlockTools.find((tool) => tool.name === block.name);\n\n  if (blockTool !== undefined && !isToolConvertable(blockTool, 'export')) {\n    return [];\n  }\n\n  return allBlockTools.reduce((result, tool) => {\n    /**\n     * Skip tools without «import» rule specified\n     */\n    if (!isToolConvertable(tool, 'import')) {\n      return result;\n    }\n\n    /**\n     * Skip tools that does not specify toolbox\n     */\n    if (tool.toolbox === undefined) {\n      return result;\n    }\n\n    /** Filter out invalid toolbox entries */\n    const actualToolboxItems = tool.toolbox.filter((toolboxItem) => {\n      /**\n       * Skip items that don't pass 'toolbox' property or do not have an icon\n       */\n      if (isEmpty(toolboxItem) || toolboxItem.icon === undefined) {\n        return false;\n      }\n\n      if (toolboxItem.data !== undefined) {\n        /**\n         * When a tool has several toolbox entries, we need to make sure we do not add\n         * toolbox item with the same data to the resulting array. This helps exclude duplicates\n         */\n        if (isSameBlockData(toolboxItem.data, blockData)) {\n          return false;\n        }\n      } else if (tool.name === block.name) {\n        return false;\n      }\n\n      return true;\n    });\n\n    result.push({\n      ...tool,\n      toolbox: actualToolboxItems,\n    } as BlockToolAdapter);\n\n    return result;\n  }, [] as BlockToolAdapter[]);\n}\n\n\n/**\n * Check if two blocks could be merged.\n *\n * We can merge two blocks if:\n *  - they have the same type\n *  - they have a merge function (.mergeable = true)\n *  - If they have valid conversions config\n *\n * @param targetBlock - block to merge to\n * @param blockToMerge - block to merge from\n */\nexport function areBlocksMergeable(targetBlock: Block, blockToMerge: Block): boolean {\n  /**\n   * If target block has not 'merge' method, we can't merge blocks.\n   *\n   * Technically we can (through the conversion) but it will lead a target block delete and recreation, which is unexpected behavior.\n   */\n  if (!targetBlock.mergeable) {\n    return false;\n  }\n\n  /**\n   * Tool knows how to merge own data format\n   */\n  if (targetBlock.name === blockToMerge.name) {\n    return true;\n  }\n\n  /**\n   * We can merge blocks if they have valid conversion config\n   */\n  return isBlockConvertable(blockToMerge, 'export') && isBlockConvertable(targetBlock, 'import');\n}\n\n/**\n * Using conversionConfig, convert block data to string.\n *\n * @param blockData - block data to convert\n * @param conversionConfig - tool's conversion config\n */\nexport function convertBlockDataToString(blockData: BlockToolData, conversionConfig?: ConversionConfig ): string {\n  const exportProp = conversionConfig?.export;\n\n  if (isFunction(exportProp)) {\n    return exportProp(blockData);\n  } else if (isString(exportProp)) {\n    return blockData[exportProp];\n  } else {\n    /**\n     * Tool developer provides 'export' property, but it is not correct. Warn him.\n     */\n    if (exportProp !== undefined) {\n      log('Conversion «export» property must be a string or function. ' +\n      'String means key of saved data object to export. Function should export processed string to export.');\n    }\n\n    return '';\n  }\n}\n\n/**\n * Using conversionConfig, convert string to block data.\n *\n * @param stringToImport - string to convert\n * @param conversionConfig - tool's conversion config\n * @param targetToolConfig - target tool config, used in conversionConfig.import method\n */\nexport function convertStringToBlockData(stringToImport: string, conversionConfig?: ConversionConfig, targetToolConfig?: ToolConfig): BlockToolData {\n  const importProp = conversionConfig?.import;\n\n  if (isFunction(importProp)) {\n    return importProp(stringToImport, targetToolConfig);\n  } else if (isString(importProp)) {\n    return {\n      [importProp]: stringToImport,\n    };\n  } else {\n    /**\n     * Tool developer provides 'import' property, but it is not correct. Warn him.\n     */\n    if (importProp !== undefined) {\n      log('Conversion «import» property must be a string or function. ' +\n      'String means key of tool data to import. Function accepts a imported string and return composed tool data.');\n    }\n\n    return {};\n  }\n}\n\n"
  },
  {
    "path": "src/components/utils/caret.ts",
    "content": "import $, { isCollapsedWhitespaces } from '../dom';\n\n/**\n * Returns TextNode containing a caret and a caret offset in it\n * Returns null if there is no caret set\n *\n * Handles a case when focusNode is an ElementNode and focusOffset is a child index,\n * returns child node with focusOffset index as a new focusNode\n */\nexport function getCaretNodeAndOffset(): [ Node | null, number ] {\n  const selection = window.getSelection();\n\n  if (selection === null) {\n    return [null, 0];\n  }\n\n  let focusNode = selection.focusNode;\n  let focusOffset = selection.focusOffset;\n\n  if (focusNode === null) {\n    return [null, 0];\n  }\n\n  /**\n   * Case when focusNode is an Element (or Document). In this case, focusOffset is a child index.\n   * We need to return child with focusOffset index as a new focusNode.\n   *\n   * <div>|hello</div> <---- Selection references to <div> instead of text node\n   *\n   *\n   */\n  if (focusNode.nodeType !== Node.TEXT_NODE && focusNode.childNodes.length > 0) {\n    /**\n     * In normal cases, focusOffset is a child index.\n     */\n    if (focusNode.childNodes[focusOffset]) {\n      focusNode = focusNode.childNodes[focusOffset];\n      focusOffset = 0;\n    /**\n     * But in Firefox, focusOffset can be 1 with the single child.\n     */\n    } else {\n      focusNode = focusNode.childNodes[focusOffset - 1];\n      focusOffset = focusNode.textContent.length;\n    }\n  }\n\n  return [focusNode, focusOffset];\n}\n\n/**\n * Checks content at left or right of the passed node for emptiness.\n *\n * @param contenteditable - The contenteditable element containing the nodes.\n * @param fromNode - The starting node to check from.\n * @param offsetInsideNode - The offset inside the starting node.\n * @param direction - The direction to check ('left' or 'right').\n * @returns true if adjacent content is empty, false otherwise.\n */\nexport function checkContenteditableSliceForEmptiness(contenteditable: HTMLElement, fromNode: Node, offsetInsideNode: number, direction: 'left' | 'right'): boolean {\n  const range = document.createRange();\n\n  /**\n   * In case of \"left\":\n   * Set range from the start of the contenteditable to the passed offset\n   */\n  if (direction === 'left') {\n    range.setStart(contenteditable, 0);\n    range.setEnd(fromNode, offsetInsideNode);\n\n  /**\n   * In case of \"right\":\n   * Set range from the passed offset to the end of the contenteditable\n   */\n  } else {\n    range.setStart(fromNode, offsetInsideNode);\n    range.setEnd(contenteditable, contenteditable.childNodes.length);\n  }\n\n  /**\n   * Clone the range's content and check its text content\n   */\n  const clonedContent = range.cloneContents();\n  const tempDiv = document.createElement('div');\n\n  tempDiv.appendChild(clonedContent);\n\n  const textContent = tempDiv.textContent || '';\n\n  /**\n   * In HTML there are two types of whitespaces:\n   * - visible (&nbsp;)\n   * - invisible (trailing spaces, tabs, etc.)\n   *\n   * If text contains only invisible whitespaces, it is considered to be empty\n   */\n  return isCollapsedWhitespaces(textContent);\n}\n\n/**\n * Checks if caret is at the start of the passed input\n *\n * Cases:\n *  Native input:\n *   - if offset is 0, caret is at the start\n *  Contenteditable:\n *   - caret at the first text node and offset is 0 — caret is at the start\n *   - caret not at the first text node — we need to check left siblings for emptiness\n *   - caret offset > 0, but all left part is visible (nbsp) — caret is not at the start\n *   - caret offset > 0, but all left part is invisible (whitespaces) — caret is at the start\n *\n * @param input - input where caret should be checked\n */\nexport function isCaretAtStartOfInput(input: HTMLElement): boolean {\n  const firstNode = $.getDeepestNode(input);\n\n  if (firstNode === null || $.isEmpty(input)) {\n    return true;\n  }\n\n  /**\n   * In case of native input, we simply check if offset is 0\n   */\n  if ($.isNativeInput(firstNode)) {\n    return (firstNode as HTMLInputElement).selectionEnd === 0;\n  }\n\n  if ($.isEmpty(input)) {\n    return true;\n  }\n\n  const [caretNode, caretOffset] = getCaretNodeAndOffset();\n\n  /**\n   * If there is no selection, caret is not at the start\n   */\n  if (caretNode === null) {\n    return false;\n  }\n\n  /**\n   * If there is nothing visible to the left of the caret, it is considered to be at the start\n   */\n  return checkContenteditableSliceForEmptiness(input, caretNode, caretOffset, 'left');\n}\n\n/**\n * Checks if caret is at the end of the passed input\n *\n * Cases:\n * Native input:\n * - if offset is equal to value length, caret is at the end\n * Contenteditable:\n * - caret at the last text node and offset is equal to text length — caret is at the end\n * - caret not at the last text node — we need to check right siblings for emptiness\n * - caret offset < text length, but all right part is visible (nbsp) — caret is at the end\n * - caret offset < text length, but all right part is invisible (whitespaces) — caret is at the end\n *\n * @param input - input where caret should be checked\n */\nexport function isCaretAtEndOfInput(input: HTMLElement): boolean {\n  const lastNode = $.getDeepestNode(input, true);\n\n  if (lastNode === null) {\n    return true;\n  }\n\n  /**\n   * In case of native input, we simply check if offset is equal to value length\n   */\n  if ($.isNativeInput(lastNode)) {\n    return (lastNode as HTMLInputElement).selectionEnd === (lastNode as HTMLInputElement).value.length;\n  }\n\n  const [caretNode, caretOffset] = getCaretNodeAndOffset();\n\n  /**\n   * If there is no selection, caret is not at the end\n   */\n  if (caretNode === null) {\n    return false;\n  }\n\n  /**\n   * If there is nothing visible to the right of the caret, it is considered to be at the end\n   */\n  return checkContenteditableSliceForEmptiness(input, caretNode, caretOffset, 'right');\n}\n"
  },
  {
    "path": "src/components/utils/events.ts",
    "content": "import { isEmpty } from '../utils';\n\n/**\n * Event Dispatcher event listener\n */\ntype Listener<Data> = (data: Data) => void;\n\n/**\n * Mapped type with subscriptions list\n *\n * event name -> array of callbacks\n */\ntype Subscriptions<EventMap> = {\n  [Key in keyof EventMap]: Listener<EventMap[Key]>[];\n};\n\n/**\n * Provides methods for working with Event Bus:\n *    - {Function} on - appends subscriber to the event. If event doesn't exist - creates new one\n *    - {Function} emit - fires all subscribers with data\n *    - {Function off - unsubscribes callback\n */\nexport default class EventsDispatcher<EventMap> {\n  /**\n   * All subscribers grouped by event name\n   * Object with events` names as key and array of callback functions as value\n   */\n  private subscribers = <Subscriptions<EventMap>>{};\n\n  /**\n   * Subscribe any event on callback\n   *\n   * @param eventName - event name\n   * @param callback - subscriber\n   */\n  public on<Name extends keyof EventMap>(eventName: Name, callback: Listener<EventMap[Name]>): void {\n    if (!(eventName in this.subscribers)) {\n      this.subscribers[eventName] = [];\n    }\n\n    // group by events\n    this.subscribers[eventName].push(callback);\n  }\n\n  /**\n   * Subscribe any event on callback. Callback will be called once and be removed from subscribers array after call.\n   *\n   * @param eventName - event name\n   * @param callback - subscriber\n   */\n  public once<Name extends keyof EventMap>(eventName: Name, callback: Listener<EventMap[Name]>): void {\n    if (!(eventName in this.subscribers)) {\n      this.subscribers[eventName] = [];\n    }\n\n    const wrappedCallback = (data: EventMap[typeof eventName]): void => {\n      const result = callback(data);\n\n      const indexOfHandler = this.subscribers[eventName].indexOf(wrappedCallback);\n\n      if (indexOfHandler !== -1) {\n        this.subscribers[eventName].splice(indexOfHandler, 1);\n      }\n\n      return result;\n    };\n\n    // group by events\n    this.subscribers[eventName].push(wrappedCallback);\n  }\n\n  /**\n   * Emit callbacks with passed data\n   *\n   * @param eventName - event name\n   * @param data - subscribers get this data when they were fired\n   */\n  public emit<Name extends keyof EventMap>(eventName: Name, data?: EventMap[Name]): void {\n    if (isEmpty(this.subscribers) || !this.subscribers[eventName]) {\n      return;\n    }\n\n    this.subscribers[eventName].reduce((previousData, currentHandler) => {\n      const newData = currentHandler(previousData);\n\n      return newData !== undefined ? newData : previousData;\n    }, data);\n  }\n\n  /**\n   * Unsubscribe callback from event\n   *\n   * @param eventName - event name\n   * @param callback - event handler\n   */\n  public off<Name extends keyof EventMap>(eventName: Name, callback: Listener<EventMap[Name]>): void {\n    if (this.subscribers[eventName] === undefined) {\n      console.warn(`EventDispatcher .off(): there is no subscribers for event \"${eventName.toString()}\". Probably, .off() called before .on()`);\n\n      return;\n    }\n\n    for (let i = 0; i < this.subscribers[eventName].length; i++) {\n      if (this.subscribers[eventName][i] === callback) {\n        delete this.subscribers[eventName][i];\n        break;\n      }\n    }\n  }\n\n  /**\n   * Destroyer\n   * clears subscribers list\n   */\n  public destroy(): void {\n    this.subscribers = {} as Subscriptions<EventMap>;\n  }\n}\n"
  },
  {
    "path": "src/components/utils/keyboard.ts",
    "content": "declare global {\n  /**\n   * https://developer.mozilla.org/en-US/docs/Web/API/KeyboardLayoutMap\n   */\n  interface KeyboardLayoutMap {\n    get(key: string): string | undefined;\n    has(key: string): boolean;\n    size: number;\n    entries(): IterableIterator<[string, string]>;\n    keys(): IterableIterator<string>;\n    values(): IterableIterator<string>;\n    forEach(callbackfn: (value: string, key: string, map: KeyboardLayoutMap) => void, thisArg?: unknown): void;\n  }\n\n  /**\n   * The getLayoutMap() method of the Keyboard interface returns a Promise\n   * that resolves with an instance of KeyboardLayoutMap which is a map-like object\n   * with functions for retrieving the strings associated with specific physical keys.\n   * https://developer.mozilla.org/en-US/docs/Web/API/Keyboard/getLayoutMap\n   */\n  interface Keyboard {\n    getLayoutMap(): Promise<KeyboardLayoutMap>;\n  }\n\n  interface Navigator {\n    /**\n     * Keyboard API. Not supported by Firefox and Safari.\n     */\n    keyboard?: Keyboard;\n  }\n}\n\n/**\n * Returns real layout-related keyboard key for a given key code.\n * For example, for \"Slash\" it will return \"/\" on US keyboard and \"-\" on Spanish keyboard.\n *\n * Works with Keyboard API which is not supported by Firefox and Safari. So fallback is used for these browsers.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Keyboard\n * @param code - {@link https://www.w3.org/TR/uievents-code/#key-alphanumeric-writing-system}\n * @param fallback - fallback value to be returned if Keyboard API is not supported (Safari, Firefox)\n */\nexport async function getKeyboardKeyForCode(code: string, fallback: string): Promise<string> {\n  const keyboard = navigator.keyboard;\n\n  if (!keyboard) {\n    return fallback;\n  }\n\n  try {\n    const map = await keyboard.getLayoutMap();\n\n    const key = map.get(code);\n\n    return key || fallback;\n  } catch (e) {\n    console.error(e);\n\n    return fallback;\n  }\n}\n"
  },
  {
    "path": "src/components/utils/listeners.ts",
    "content": "import * as _ from '../utils';\n\n/**\n * Event listener information\n *\n * @interface ListenerData\n */\nexport interface ListenerData {\n  /**\n   * Listener unique identifier\n   */\n  id: string;\n\n  /**\n   * Element where to listen to dispatched events\n   */\n  element: EventTarget;\n\n  /**\n   * Event to listen\n   */\n  eventType: string;\n\n  /**\n   * Event handler\n   *\n   * @param {Event} event - event object\n   */\n  handler: (event: Event) => void;\n\n  /**\n   * @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener\n   */\n  options: boolean | AddEventListenerOptions;\n}\n\n/**\n * Editor.js Listeners helper\n *\n * Decorator for event listeners assignment\n *\n * @author Codex Team\n * @version 2.0.0\n */\n\n/**\n * @typedef {Listeners} Listeners\n * @property {ListenerData[]} allListeners - listeners store\n */\nexport default class Listeners {\n  /**\n   * Stores all listeners data to find/remove/process it\n   *\n   * @type {ListenerData[]}\n   */\n  private allListeners: ListenerData[] = [];\n\n  /**\n   * Assigns event listener on element and returns unique identifier\n   *\n   * @param {EventTarget} element - DOM element that needs to be listened\n   * @param {string} eventType - event type\n   * @param {Function} handler - method that will be fired on event\n   * @param {boolean|AddEventListenerOptions} options - useCapture or {capture, passive, once}\n   */\n  public on(\n    element: EventTarget,\n    eventType: string,\n    handler: (event: Event) => void,\n    options: boolean | AddEventListenerOptions = false\n  ): string {\n    const id = _.generateId('l');\n    const assignedEventData = {\n      id,\n      element,\n      eventType,\n      handler,\n      options,\n    };\n\n    const alreadyExist = this.findOne(element, eventType, handler);\n\n    if (alreadyExist) {\n      return;\n    }\n\n    this.allListeners.push(assignedEventData);\n    element.addEventListener(eventType, handler, options);\n\n    return id;\n  }\n\n  /**\n   * Removes event listener from element\n   *\n   * @param {EventTarget} element - DOM element that we removing listener\n   * @param {string} eventType - event type\n   * @param {Function} handler - remove handler, if element listens several handlers on the same event type\n   * @param {boolean|AddEventListenerOptions} options - useCapture or {capture, passive, once}\n   */\n  public off(\n    element: EventTarget,\n    eventType: string,\n    handler?: (event: Event) => void,\n    // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars\n    options?: boolean | AddEventListenerOptions\n  ): void {\n    const existingListeners = this.findAll(element, eventType, handler);\n\n    existingListeners.forEach((listener, i) => {\n      const index = this.allListeners.indexOf(existingListeners[i]);\n\n      if (index > -1) {\n        this.allListeners.splice(index, 1);\n\n        listener.element.removeEventListener(listener.eventType, listener.handler, listener.options);\n      }\n    });\n  }\n\n  /**\n   * Removes listener by id\n   *\n   * @param {string} id - listener identifier\n   */\n  public offById(id: string): void {\n    const listener = this.findById(id);\n\n    if (!listener) {\n      return;\n    }\n\n    listener.element.removeEventListener(listener.eventType, listener.handler, listener.options);\n  }\n\n  /**\n   * Finds and returns first listener by passed params\n   *\n   * @param {EventTarget} element - event target\n   * @param {string} [eventType] - event type\n   * @param {Function} [handler] - event handler\n   * @returns {ListenerData|null}\n   */\n  public findOne(element: EventTarget, eventType?: string, handler?: (event: Event) => void): ListenerData {\n    const foundListeners = this.findAll(element, eventType, handler);\n\n    return foundListeners.length > 0 ? foundListeners[0] : null;\n  }\n\n  /**\n   * Return all stored listeners by passed params\n   *\n   * @param {EventTarget} element - event target\n   * @param {string} eventType - event type\n   * @param {Function} handler - event handler\n   * @returns {ListenerData[]}\n   */\n  public findAll(element: EventTarget, eventType?: string, handler?: (event: Event) => void): ListenerData[] {\n    let found;\n    const foundByEventTargets = element ? this.findByEventTarget(element) : [];\n\n    if (element && eventType && handler) {\n      found = foundByEventTargets.filter((event) => event.eventType === eventType && event.handler === handler);\n    } else if (element && eventType) {\n      found = foundByEventTargets.filter((event) => event.eventType === eventType);\n    } else {\n      found = foundByEventTargets;\n    }\n\n    return found;\n  }\n\n  /**\n   * Removes all listeners\n   */\n  public removeAll(): void {\n    this.allListeners.map((current) => {\n      current.element.removeEventListener(current.eventType, current.handler, current.options);\n    });\n\n    this.allListeners = [];\n  }\n\n  /**\n   * Module cleanup on destruction\n   */\n  public destroy(): void {\n    this.removeAll();\n  }\n\n  /**\n   * Search method: looks for listener by passed element\n   *\n   * @param {EventTarget} element - searching element\n   * @returns {Array} listeners that found on element\n   */\n  private findByEventTarget(element: EventTarget): ListenerData[] {\n    return this.allListeners.filter((listener) => {\n      if (listener.element === element) {\n        return listener;\n      }\n    });\n  }\n\n  /**\n   * Search method: looks for listener by passed event type\n   *\n   * @param {string} eventType - event type\n   * @returns {ListenerData[]} listeners that found on element\n   */\n  private findByType(eventType: string): ListenerData[] {\n    return this.allListeners.filter((listener) => {\n      if (listener.eventType === eventType) {\n        return listener;\n      }\n    });\n  }\n\n  /**\n   * Search method: looks for listener by passed handler\n   *\n   * @param {Function} handler - event handler\n   * @returns {ListenerData[]} listeners that found on element\n   */\n  private findByHandler(handler: (event: Event) => void): ListenerData[] {\n    return this.allListeners.filter((listener) => {\n      if (listener.handler === handler) {\n        return listener;\n      }\n    });\n  }\n\n  /**\n   * Returns listener data found by id\n   *\n   * @param {string} id - listener identifier\n   * @returns {ListenerData}\n   */\n  private findById(id: string): ListenerData {\n    return this.allListeners.find((listener) => listener.id === id);\n  }\n}\n"
  },
  {
    "path": "src/components/utils/mutations.ts",
    "content": "/**\n * Check if passed mutation belongs to a passed element\n *\n * @param mutationRecord - mutation to check\n * @param element - element that is expected to contain mutation\n */\nexport function isMutationBelongsToElement(mutationRecord: MutationRecord, element: Element): boolean {\n  const { type, target, addedNodes, removedNodes } = mutationRecord;\n\n  /**\n   * Skip own technical mutations, for example, data-empty attribute changes\n   */\n  if (mutationRecord.type === 'attributes' && mutationRecord.attributeName === 'data-empty') {\n    return false;\n  }\n\n  /**\n   * Covers all types of mutations happened to the element or it's descendants with the only one exception - removing/adding the element itself;\n   */\n  if (element.contains(target)) {\n    return true;\n  }\n\n  /**\n   * In case of removing/adding the element itself, mutation type will be 'childList' and 'removedNodes'/'addedNodes' will contain the element.\n   */\n  if (type === 'childList') {\n    const elementAddedItself = Array.from(addedNodes).some(node => node === element);\n\n    if (elementAddedItself) {\n      return true;\n    }\n\n    const elementRemovedItself = Array.from(removedNodes).some(node => node === element);\n\n    if (elementRemovedItself) {\n      return true;\n    }\n  }\n\n  return false;\n}\n"
  },
  {
    "path": "src/components/utils/notifier.ts",
    "content": "/**\n * Use external package module for notifications\n *\n * @see https://github.com/codex-team/js-notifier\n */\nimport type { ConfirmNotifierOptions, NotifierOptions, PromptNotifierOptions } from 'codex-notifier';\nimport notifier from 'codex-notifier';\n\n/**\n * Util for showing notifications\n */\nexport default class Notifier {\n  /**\n   * Show web notification\n   *\n   * @param {NotifierOptions | ConfirmNotifierOptions | PromptNotifierOptions} options - notification options\n   */\n  public show(options: NotifierOptions | ConfirmNotifierOptions | PromptNotifierOptions): void {\n    notifier.show(options);\n  }\n}\n"
  },
  {
    "path": "src/components/utils/popover/components/hint/hint.const.ts",
    "content": "import { bem } from '../../../bem';\n\n/**\n * Hint block CSS class constructor\n */\nconst className = bem('ce-hint');\n\n/**\n * CSS class names to be used in hint class\n */\nexport const css = {\n  root: className(),\n  alignedStart: className(null, 'align-left'),\n  alignedCenter: className(null, 'align-center'),\n  title: className('title'),\n  description: className('description'),\n};\n"
  },
  {
    "path": "src/components/utils/popover/components/hint/hint.css",
    "content": ".ce-hint {\n  &--align-start {\n    text-align: start;\n  }\n\n  &--align-center {\n    text-align: center;\n  }\n\n  &__description {\n    opacity: 0.6;\n    margin-top: 3px;\n  }\n}\n"
  },
  {
    "path": "src/components/utils/popover/components/hint/hint.ts",
    "content": "import Dom from '../../../../dom';\nimport { css } from './hint.const';\nimport type { HintParams } from '@/types/utils/popover/hint';\n\nimport './hint.css';\n\n/**\n * Represents the hint content component\n */\nexport class Hint {\n  /**\n   * Html element used to display hint content on screen\n   */\n  private nodes: {\n    root: HTMLElement;\n    title: HTMLElement;\n    description?: HTMLElement;\n  };\n\n  /**\n   * Constructs the hint content instance\n   *\n   * @param params - hint content parameters\n   */\n  constructor(params: HintParams) {\n    this.nodes = {\n      root: Dom.make('div', [css.root, params.alignment === 'center' ? css.alignedCenter : css.alignedStart]),\n      title: Dom.make('div', css.title, { textContent: params.title }),\n    };\n\n    this.nodes.root.appendChild(this.nodes.title);\n\n    if (params.description !== undefined) {\n      this.nodes.description = Dom.make('div', css.description, { textContent: params.description });\n\n      this.nodes.root.appendChild(this.nodes.description);\n    }\n  }\n\n  /**\n   * Returns the root element of the hint content\n   */\n  public getElement(): HTMLElement {\n    return this.nodes.root;\n  }\n}\n"
  },
  {
    "path": "src/components/utils/popover/components/hint/index.ts",
    "content": "export * from './hint';\nexport type {\n  HintParams,\n  HintPosition,\n  HintTextAlignment\n} from '@/types/utils/popover/hint';\n"
  },
  {
    "path": "src/components/utils/popover/components/popover-header/index.ts",
    "content": "export * from './popover-header';\nexport * from './popover-header.types';\n"
  },
  {
    "path": "src/components/utils/popover/components/popover-header/popover-header.const.ts",
    "content": "import { bem } from '../../../bem';\n\n/**\n * Popover header block CSS class constructor\n */\nconst className = bem('ce-popover-header');\n\n/**\n * CSS class names to be used in popover header class\n */\nexport const css = {\n  root: className(),\n  text: className('text'),\n  backButton: className('back-button'),\n};\n"
  },
  {
    "path": "src/components/utils/popover/components/popover-header/popover-header.ts",
    "content": "import type { PopoverHeaderParams } from './popover-header.types';\nimport Dom from '../../../../dom';\nimport { css } from './popover-header.const';\nimport { IconChevronLeft } from '@codexteam/icons';\nimport Listeners from '../../../listeners';\n\n/**\n * Represents popover header ui element\n */\nexport class PopoverHeader {\n  /**\n   * Listeners util instance\n   */\n  private listeners = new Listeners();\n\n  /**\n   * Header html elements\n   */\n  private nodes: {\n      root: HTMLElement,\n      text: HTMLElement,\n      backButton: HTMLElement\n    };\n\n  /**\n   * Text displayed inside header\n   */\n  private readonly text: string;\n\n  /**\n   * Back button click handler\n   */\n  private readonly onBackButtonClick: () => void;\n\n  /**\n   * Constructs the instance\n   *\n   * @param params - popover header params\n   */\n  constructor({ text, onBackButtonClick }: PopoverHeaderParams) {\n    this.text = text;\n    this.onBackButtonClick = onBackButtonClick;\n\n    this.nodes = {\n      root: Dom.make('div', [ css.root ]),\n      backButton: Dom.make('button', [ css.backButton ]),\n      text: Dom.make('div', [ css.text ]),\n    };\n    this.nodes.backButton.innerHTML = IconChevronLeft;\n    this.nodes.root.appendChild(this.nodes.backButton);\n    this.listeners.on(this.nodes.backButton, 'click', this.onBackButtonClick);\n\n    this.nodes.text.innerText = this.text;\n    this.nodes.root.appendChild(this.nodes.text);\n  }\n\n  /**\n   * Returns popover header root html element\n   */\n  public getElement(): HTMLElement | null {\n    return this.nodes.root;\n  }\n\n  /**\n   * Destroys the instance\n   */\n  public destroy(): void {\n    this.nodes.root.remove();\n    this.listeners.destroy();\n  }\n}\n"
  },
  {
    "path": "src/components/utils/popover/components/popover-header/popover-header.types.ts",
    "content": "/**\n * Popover header params\n */\nexport interface PopoverHeaderParams {\n  /**\n   * Text to be displayed inside header\n   */\n  text: string;\n\n  /**\n   * Back button click handler\n   */\n  onBackButtonClick: () => void;\n}\n"
  },
  {
    "path": "src/components/utils/popover/components/popover-item/index.ts",
    "content": "import { PopoverItemDefault } from './popover-item-default/popover-item-default';\nimport { PopoverItemSeparator } from './popover-item-separator/popover-item-separator';\nimport { PopoverItem } from './popover-item';\n\nexport * from './popover-item-default/popover-item-default.const';\nexport type * from '@/types/utils/popover/popover-item.d.ts';\nexport { PopoverItemType } from '@/types/utils/popover/popover-item-type';\n\nexport {\n  PopoverItemDefault,\n  PopoverItemSeparator,\n  PopoverItem\n};\n"
  },
  {
    "path": "src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.const.ts",
    "content": "import { bem } from '../../../../bem';\n\n/**\n * Popover item block CSS class constructor\n */\nconst className = bem('ce-popover-item');\n\n/**\n * CSS class names to be used in popover item class\n */\nexport const css = {\n  container: className(),\n  active: className(null, 'active'),\n  disabled: className(null, 'disabled'),\n  focused: className(null, 'focused'),\n  hidden: className(null, 'hidden'),\n  confirmationState: className(null, 'confirmation'),\n  noHover: className(null, 'no-hover'),\n  noFocus: className(null, 'no-focus'),\n  title: className('title'),\n  secondaryTitle: className('secondary-title'),\n  icon: className('icon'),\n  iconTool: className('icon', 'tool'),\n  iconChevronRight: className('icon', 'chevron-right'),\n  wobbleAnimation: bem('wobble')(),\n};\n"
  },
  {
    "path": "src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts",
    "content": "import Dom from '../../../../../dom';\nimport { IconDotCircle, IconChevronRight } from '@codexteam/icons';\nimport type {\n  PopoverItemDefaultParams as PopoverItemDefaultParams,\n  PopoverItemRenderParamsMap,\n  PopoverItemType\n} from '@/types/utils/popover/popover-item';\nimport { PopoverItem } from '../popover-item';\nimport { css } from './popover-item-default.const';\n\n/**\n * Represents sigle popover item node\n *\n * @todo move nodes initialization to constructor\n * @todo replace multiple make() usages with constructing separate instances\n * @todo split regular popover item and popover item with confirmation to separate classes\n * @todo display icon on the right side of the item for rtl languages\n */\nexport class PopoverItemDefault extends PopoverItem {\n  /**\n   * True if item is disabled and hence not clickable\n   */\n  public get isDisabled(): boolean {\n    return this.params.isDisabled === true;\n  }\n\n  /**\n   * Exposes popover item toggle parameter\n   */\n  public get toggle(): boolean | string | undefined {\n    return this.params.toggle;\n  }\n\n  /**\n   * Item title\n   */\n  public get title(): string | undefined {\n    return this.params.title;\n  }\n\n  /**\n   * True if confirmation state is enabled for popover item\n   */\n  public get isConfirmationStateEnabled(): boolean {\n    return this.confirmationState !== null;\n  }\n\n  /**\n   * True if item is focused in keyboard navigation process\n   */\n  public get isFocused(): boolean {\n    if (this.nodes.root === null) {\n      return false;\n    }\n\n    return this.nodes.root.classList.contains(css.focused);\n  }\n\n  /**\n   * Item html elements\n   */\n  private nodes: {\n    root: null | HTMLElement,\n    icon: null | HTMLElement\n  } = {\n      root: null,\n      icon: null,\n    };\n\n  /**\n   * If item is in confirmation state, stores confirmation params such as icon, label, onActivate callback and so on\n   */\n  private confirmationState: PopoverItemDefaultParams | null = null;\n\n  /**\n   * Constructs popover item instance\n   *\n   * @param params - popover item construction params\n   * @param renderParams - popover item render params.\n   * The parameters that are not set by user via popover api but rather depend on technical implementation\n   */\n  constructor(protected readonly params: PopoverItemDefaultParams, renderParams?: PopoverItemRenderParamsMap[PopoverItemType.Default]) {\n    super(params);\n\n    this.nodes.root = this.make(params, renderParams);\n  }\n\n  /**\n   * Returns popover item root element\n   */\n  public getElement(): HTMLElement | null {\n    return this.nodes.root;\n  }\n\n  /**\n   * Called on popover item click\n   */\n  public handleClick(): void {\n    if (this.isConfirmationStateEnabled && this.confirmationState !== null) {\n      this.activateOrEnableConfirmationMode(this.confirmationState);\n\n      return;\n    }\n\n    this.activateOrEnableConfirmationMode(this.params);\n  }\n\n  /**\n   * Toggles item active state\n   *\n   * @param isActive - true if item should strictly should become active\n   */\n  public toggleActive(isActive?: boolean): void {\n    this.nodes.root?.classList.toggle(css.active, isActive);\n  }\n\n  /**\n   * Toggles item hidden state\n   *\n   * @param isHidden - true if item should be hidden\n   */\n  public override toggleHidden(isHidden: boolean): void {\n    this.nodes.root?.classList.toggle(css.hidden, isHidden);\n  }\n\n  /**\n   * Resets popover item to its original state\n   */\n  public reset(): void {\n    if (this.isConfirmationStateEnabled) {\n      this.disableConfirmationMode();\n    }\n  }\n\n  /**\n   * Method called once item becomes focused during keyboard navigation\n   */\n  public onFocus(): void {\n    this.disableSpecialHoverAndFocusBehavior();\n  }\n\n  /**\n   * Constructs HTML element corresponding to popover item params\n   *\n   * @param params - item construction params\n   * @param renderParams - popover item render params\n   */\n  private make(params: PopoverItemDefaultParams, renderParams?: PopoverItemRenderParamsMap[PopoverItemType.Default]): HTMLElement {\n    const tag = renderParams?.wrapperTag || 'div';\n    const el = Dom.make(tag, css.container, {\n      type: tag === 'button' ? 'button' : undefined,\n    });\n\n    if (params.name) {\n      el.dataset.itemName = params.name;\n    }\n\n    this.nodes.icon = Dom.make('div', [css.icon, css.iconTool], {\n      innerHTML: params.icon || IconDotCircle,\n    });\n\n    el.appendChild(this.nodes.icon);\n\n    if (params.title !== undefined) {\n      el.appendChild(Dom.make('div', css.title, {\n        innerHTML: params.title || '',\n      }));\n    }\n\n    if (params.secondaryLabel) {\n      el.appendChild(Dom.make('div', css.secondaryTitle, {\n        textContent: params.secondaryLabel,\n      }));\n    }\n\n    if (this.hasChildren) {\n      el.appendChild(Dom.make('div', [css.icon, css.iconChevronRight], {\n        innerHTML: IconChevronRight,\n      }));\n    }\n\n    if (this.isActive) {\n      el.classList.add(css.active);\n    }\n\n    if (params.isDisabled) {\n      el.classList.add(css.disabled);\n    }\n\n    if (params.hint !== undefined && renderParams?.hint?.enabled !== false) {\n      this.addHint(el, {\n        ...params.hint,\n        position: renderParams?.hint?.position || 'right',\n      });\n    }\n\n    return el;\n  }\n\n  /**\n   * Activates confirmation mode for the item.\n   *\n   * @param newState - new popover item params that should be applied\n   */\n  private enableConfirmationMode(newState: PopoverItemDefaultParams): void {\n    if (this.nodes.root === null) {\n      return;\n    }\n\n    const params = {\n      ...this.params,\n      ...newState,\n      confirmation: 'confirmation' in newState ? newState.confirmation : undefined,\n    } as PopoverItemDefaultParams;\n    const confirmationEl = this.make(params);\n\n    this.nodes.root.innerHTML = confirmationEl.innerHTML;\n    this.nodes.root.classList.add(css.confirmationState);\n\n    this.confirmationState = newState;\n\n    this.enableSpecialHoverAndFocusBehavior();\n  }\n\n  /**\n   * Returns item to its original state\n   */\n  private disableConfirmationMode(): void {\n    if (this.nodes.root === null) {\n      return;\n    }\n    const itemWithOriginalParams = this.make(this.params);\n\n    this.nodes.root.innerHTML = itemWithOriginalParams.innerHTML;\n    this.nodes.root.classList.remove(css.confirmationState);\n\n    this.confirmationState = null;\n\n    this.disableSpecialHoverAndFocusBehavior();\n  }\n\n  /**\n   * Enables special focus and hover behavior for item in confirmation state.\n   * This is needed to prevent item from being highlighted as hovered/focused just after click.\n   */\n  private enableSpecialHoverAndFocusBehavior(): void {\n    this.nodes.root?.classList.add(css.noHover);\n    this.nodes.root?.classList.add(css.noFocus);\n\n    this.nodes.root?.addEventListener('mouseleave', this.removeSpecialHoverBehavior, { once: true });\n  }\n\n  /**\n   * Disables special focus and hover behavior\n   */\n  private disableSpecialHoverAndFocusBehavior(): void  {\n    this.removeSpecialFocusBehavior();\n    this.removeSpecialHoverBehavior();\n\n    this.nodes.root?.removeEventListener('mouseleave', this.removeSpecialHoverBehavior);\n  }\n\n  /**\n   * Removes class responsible for special focus behavior on an item\n   */\n  private removeSpecialFocusBehavior = (): void => {\n    this.nodes.root?.classList.remove(css.noFocus);\n  };\n\n  /**\n   * Removes class responsible for special hover behavior on an item\n   */\n  private removeSpecialHoverBehavior = (): void => {\n    this.nodes.root?.classList.remove(css.noHover);\n  };\n\n  /**\n   * Executes item's onActivate callback if the item has no confirmation configured\n   *\n   * @param item - item to activate or bring to confirmation mode\n   */\n  private activateOrEnableConfirmationMode(item: PopoverItemDefaultParams): void {\n    if (!('confirmation' in item) || item.confirmation === undefined) {\n      try {\n        item.onActivate?.(item);\n        this.disableConfirmationMode();\n      } catch {\n        this.animateError();\n      }\n    } else {\n      this.enableConfirmationMode(item.confirmation);\n    }\n  }\n\n  /**\n   * Animates item which symbolizes that error occured while executing 'onActivate()' callback\n   */\n  private animateError(): void {\n    if (this.nodes.icon?.classList.contains(css.wobbleAnimation)) {\n      return;\n    }\n\n    this.nodes.icon?.classList.add(css.wobbleAnimation);\n\n    this.nodes.icon?.addEventListener('animationend', this.onErrorAnimationEnd);\n  }\n\n  /**\n   * Handles finish of error animation\n   */\n  private onErrorAnimationEnd = (): void => {\n    this.nodes.icon?.classList.remove(css.wobbleAnimation);\n    this.nodes.icon?.removeEventListener('animationend', this.onErrorAnimationEnd);\n  };\n}\n"
  },
  {
    "path": "src/components/utils/popover/components/popover-item/popover-item-html/popover-item-html.const.ts",
    "content": "import { bem } from '../../../../bem';\n\n/**\n * Popover item block CSS class constructor\n */\nconst className = bem('ce-popover-item-html');\n\n/**\n * CSS class names to be used in popover item class\n */\nexport const css = {\n  root: className(),\n  hidden: className(null, 'hidden'),\n};\n"
  },
  {
    "path": "src/components/utils/popover/components/popover-item/popover-item-html/popover-item-html.ts",
    "content": "import { PopoverItem } from '../popover-item';\nimport type { PopoverItemHtmlParams, PopoverItemRenderParamsMap, PopoverItemType } from '@/types/utils/popover/popover-item';\nimport { css } from './popover-item-html.const';\nimport Dom from '../../../../../dom';\n\n/**\n * Represents popover item with custom html content\n */\nexport class PopoverItemHtml extends PopoverItem {\n  /**\n   * Item html elements\n   */\n  private nodes: { root: HTMLElement };\n\n  /**\n   * Constructs the instance\n   *\n   * @param params – instance parameters\n   * @param renderParams – popover item render params.\n   * The parameters that are not set by user via popover api but rather depend on technical implementation\n   */\n  constructor(params: PopoverItemHtmlParams, renderParams?: PopoverItemRenderParamsMap[PopoverItemType.Html]) {\n    super(params);\n\n    this.nodes = {\n      root: Dom.make('div', css.root),\n    };\n\n    this.nodes.root.appendChild(params.element);\n\n    if (params.name) {\n      this.nodes.root.dataset.itemName = params.name;\n    }\n\n    if (params.hint !== undefined && renderParams?.hint?.enabled !== false) {\n      this.addHint(this.nodes.root, {\n        ...params.hint,\n        position: renderParams?.hint?.position || 'right',\n      });\n    }\n  }\n\n  /**\n   * Returns popover item root element\n   */\n  public getElement(): HTMLElement {\n    return this.nodes.root;\n  }\n\n  /**\n   * Toggles item hidden state\n   *\n   * @param isHidden - true if item should be hidden\n   */\n  public toggleHidden(isHidden: boolean): void {\n    this.nodes.root?.classList.toggle(css.hidden, isHidden);\n  }\n\n  /**\n   * Returns list of buttons and inputs inside custom content\n   */\n  public getControls(): HTMLElement[] {\n    /** Query buttons and inputs inside custom html */\n    const controls = this.nodes.root.querySelectorAll<HTMLElement>(\n      `button, ${Dom.allInputsSelector}`\n    );\n\n    return Array.from(controls);\n  }\n}\n"
  },
  {
    "path": "src/components/utils/popover/components/popover-item/popover-item-separator/popover-item-separator.const.ts",
    "content": "import { bem } from '../../../../bem';\n\n/**\n * Popover separator block CSS class constructor\n */\nconst className = bem('ce-popover-item-separator');\n\n/**\n * CSS class names to be used in popover separator class\n */\nexport const css = {\n  container: className(),\n  line: className('line'),\n  hidden: className(null, 'hidden'),\n};\n"
  },
  {
    "path": "src/components/utils/popover/components/popover-item/popover-item-separator/popover-item-separator.ts",
    "content": "import Dom from '../../../../../dom';\nimport { PopoverItem } from '../popover-item';\nimport { css } from './popover-item-separator.const';\n\n/**\n * Represents popover separator node\n */\nexport class PopoverItemSeparator extends PopoverItem {\n  /**\n   * Html elements\n   */\n  private nodes: { root: HTMLElement; line: HTMLElement };\n\n  /**\n   * Constructs the instance\n   */\n  constructor() {\n    super();\n\n    this.nodes = {\n      root: Dom.make('div', css.container),\n      line: Dom.make('div', css.line),\n    };\n\n    this.nodes.root.appendChild(this.nodes.line);\n  }\n\n  /**\n   * Returns popover separator root element\n   */\n  public getElement(): HTMLElement {\n    return this.nodes.root;\n  }\n\n  /**\n   * Toggles item hidden state\n   *\n   * @param isHidden - true if item should be hidden\n   */\n  public toggleHidden(isHidden: boolean): void {\n    this.nodes.root?.classList.toggle(css.hidden, isHidden);\n  }\n}\n"
  },
  {
    "path": "src/components/utils/popover/components/popover-item/popover-item.ts",
    "content": "import * as tooltip from '../../../../utils/tooltip';\nimport { type HintPosition, Hint } from '../hint';\nimport type { PopoverItemParams } from '@/types/utils/popover/popover-item';\n\n/**\n * Popover item abstract class\n */\nexport abstract class PopoverItem {\n  /**\n   * Constructs the instance\n   *\n   * @param params - instance parameters\n   */\n  constructor(protected readonly params?: PopoverItemParams) {}\n\n  /**\n   * Item name if exists\n   */\n  public get name(): string | undefined {\n    if (this.params === undefined) {\n      return;\n    }\n    if ('name' in this.params) {\n      return this.params.name;\n    }\n  }\n\n  /**\n   * Destroys the instance\n   */\n  public destroy(): void {\n    tooltip.hide();\n  }\n\n  /**\n   * Called when children popover is opened (if exists)\n   */\n  public onChildrenOpen(): void {\n    if (this.params === undefined) {\n      return;\n    }\n\n    if ('children' in this.params && typeof this.params.children?.onOpen === 'function') {\n      this.params.children.onOpen();\n    }\n  }\n\n  /**\n   * Called when children popover is closed (if exists)\n   */\n  public onChildrenClose(): void {\n    if (this.params === undefined) {\n      return;\n    }\n\n    if ('children' in this.params && typeof this.params.children?.onClose === 'function') {\n      this.params.children.onClose();\n    }\n  }\n\n  /**\n   * Called on popover item click\n   */\n  public handleClick(): void {\n    if (this.params === undefined) {\n      return;\n    }\n\n    if (!('onActivate' in this.params)) {\n      return;\n    }\n\n    this.params.onActivate?.(this.params);\n  }\n\n  /**\n   * Adds hint to the item element if hint data is provided\n   *\n   * @param itemElement - popover item root element to add hint to\n   * @param hintData - hint data\n   */\n  protected addHint(itemElement: HTMLElement, hintData: { title: string, description?: string; position: HintPosition }): void {\n    const content = new Hint(hintData);\n\n    tooltip.onHover(itemElement, content.getElement(), {\n      placement: hintData.position,\n      hidingDelay: 100,\n    });\n  }\n\n  /**\n   * Returns popover item root element\n   */\n  public abstract getElement(): HTMLElement | null;\n\n  /**\n   * Toggles item hidden state\n   *\n   * @param isHidden - true if item should be hidden\n   */\n  public abstract toggleHidden(isHidden: boolean): void;\n\n\n  /**\n   * Returns item children that are represented as popover items\n   */\n  public get children(): PopoverItemParams[] {\n    return this.params !== undefined && 'children' in this.params && this.params.children?.items !== undefined ? this.params.children.items : [];\n  }\n\n  /**\n   * Returns true if item has any type of children\n   */\n  public get hasChildren(): boolean {\n    return this.children.length > 0;\n  }\n\n  /**\n   * Returns true if item children should be open instantly after popover is opened and not on item click/hover\n   */\n  public get isChildrenOpen(): boolean {\n    return this.params !== undefined && 'children' in this.params && this.params.children?.isOpen === true;\n  }\n\n  /**\n   * True if item children items should be navigatable via keyboard\n   */\n  public get isChildrenFlippable(): boolean {\n    if (this.params === undefined) {\n      return false;\n    }\n\n    if (!('children' in this.params)) {\n      return false;\n    }\n\n    if (this.params.children?.isFlippable === false) {\n      return false;\n    }\n\n    return true;\n  }\n\n  /**\n   * Returns true if item has children that should be searchable\n   */\n  public get isChildrenSearchable(): boolean {\n    return this.params !== undefined && 'children' in this.params && this.params.children?.searchable === true;\n  }\n\n  /**\n   * True if popover should close once item is activated\n   */\n  public get closeOnActivate(): boolean | undefined {\n    return this.params !== undefined && 'closeOnActivate' in this.params && this.params.closeOnActivate;\n  }\n\n  /**\n   * True if item is active\n   */\n  public get isActive(): boolean {\n    if (this.params === undefined) {\n      return false;\n    }\n\n    if (!('isActive' in this.params)) {\n      return false;\n    }\n\n    if (typeof this.params.isActive === 'function') {\n      return this.params.isActive();\n    }\n\n    return this.params.isActive === true;\n  }\n}\n"
  },
  {
    "path": "src/components/utils/popover/components/search-input/index.ts",
    "content": "export * from './search-input';\nexport * from './search-input.types';\n"
  },
  {
    "path": "src/components/utils/popover/components/search-input/search-input.const.ts",
    "content": "import { bem } from '../../../bem';\n\n/**\n * Popover search input block CSS class constructor\n */\nconst className = bem('cdx-search-field');\n\n/**\n * CSS class names to be used in popover search input class\n */\nexport const css = {\n  wrapper: className(),\n  icon: className('icon'),\n  input: className('input'),\n};\n"
  },
  {
    "path": "src/components/utils/popover/components/search-input/search-input.ts",
    "content": "import Dom from '../../../../dom';\nimport Listeners from '../../../listeners';\nimport { IconSearch } from '@codexteam/icons';\nimport type { SearchInputEventMap, SearchableItem } from './search-input.types';\nimport { SearchInputEvent } from './search-input.types';\nimport { css } from './search-input.const';\nimport EventsDispatcher from '../../../events';\n\n/**\n * Provides search input element and search logic\n */\nexport class SearchInput extends EventsDispatcher<SearchInputEventMap> {\n  /**\n   * Input wrapper element\n   */\n  private wrapper: HTMLElement;\n\n  /**\n   * Editable input itself\n   */\n  private input: HTMLInputElement;\n\n  /**\n   * The instance of the Listeners util\n   */\n  private listeners: Listeners;\n\n  /**\n   * Items for local search\n   */\n  private items: SearchableItem[];\n\n  /**\n   * Current search query\n   */\n  private searchQuery: string | undefined;\n\n  /**\n   * @param options - available config\n   * @param options.items - searchable items list\n   * @param options.placeholder - input placeholder\n   */\n  constructor({ items, placeholder }: {\n    items: SearchableItem[];\n    placeholder?: string;\n  }) {\n    super();\n\n    this.listeners = new Listeners();\n    this.items = items;\n\n    /** Build ui */\n    this.wrapper = Dom.make('div', css.wrapper);\n\n    const iconWrapper = Dom.make('div', css.icon, {\n      innerHTML: IconSearch,\n    });\n\n    this.input = Dom.make('input', css.input, {\n      placeholder,\n      /**\n       * Used to prevent focusing on the input by Tab key\n       * (Popover in the Toolbar lays below the blocks,\n       * so Tab in the last block will focus this hidden input if this property is not set)\n       */\n      tabIndex: -1,\n    }) as HTMLInputElement;\n\n    this.wrapper.appendChild(iconWrapper);\n    this.wrapper.appendChild(this.input);\n\n    this.listeners.on(this.input, 'input', () => {\n      this.searchQuery = this.input.value;\n\n      this.emit(SearchInputEvent.Search, {\n        query: this.searchQuery,\n        items: this.foundItems,\n      });\n    });\n  }\n\n  /**\n   * Returns search field element\n   */\n  public getElement(): HTMLElement {\n    return this.wrapper;\n  }\n\n  /**\n   * Sets focus to the input\n   */\n  public focus(): void {\n    this.input.focus();\n  }\n\n  /**\n   * Clears search query and results\n   */\n  public clear(): void {\n    this.input.value = '';\n    this.searchQuery = '';\n\n    this.emit(SearchInputEvent.Search, {\n      query: '',\n      items: this.foundItems,\n    });\n  }\n\n  /**\n   * Clears memory\n   */\n  public destroy(): void {\n    this.listeners.removeAll();\n  }\n\n  /**\n   * Returns list of found items for the current search query\n   */\n  private get foundItems(): SearchableItem[] {\n    return this.items.filter(item => this.checkItem(item));\n  }\n\n  /**\n   * Contains logic for checking whether passed item conforms the search query\n   *\n   * @param item - item to be checked\n   */\n  private checkItem(item: SearchableItem): boolean {\n    const text = item.title?.toLowerCase() || '';\n    const query = this.searchQuery?.toLowerCase();\n\n    return query !== undefined ? text.includes(query) : false;\n  }\n}\n"
  },
  {
    "path": "src/components/utils/popover/components/search-input/search-input.types.ts",
    "content": "/**\n * Item that could be searched\n */\nexport interface SearchableItem {\n  /**\n   * Items title\n   */\n  title?: string;\n}\n\n\n/**\n * Event that can be triggered by the Search Input\n */\nexport enum SearchInputEvent {\n  /**\n   * When search quert applied\n   */\n  Search = 'search'\n}\n\n/**\n * Events fired by the Search Input\n */\nexport interface SearchInputEventMap {\n  /**\n   * Fired when search quert applied\n   */\n  [SearchInputEvent.Search]: { query: string; items: SearchableItem[]};\n}\n"
  },
  {
    "path": "src/components/utils/popover/index.ts",
    "content": "import { PopoverDesktop } from './popover-desktop';\nimport { PopoverInline } from './popover-inline';\nimport { PopoverMobile } from './popover-mobile';\n\nexport type * from '@/types/utils/popover';\nexport { PopoverItemType } from '@/types/utils/popover/popover-item-type';\n\n/**\n * Union type for all popovers\n */\nexport type Popover = PopoverDesktop | PopoverMobile | PopoverInline;\n\nexport { PopoverDesktop, PopoverMobile, PopoverInline };\n"
  },
  {
    "path": "src/components/utils/popover/popover-abstract.ts",
    "content": "import type { PopoverItem, PopoverItemRenderParamsMap } from './components/popover-item';\nimport { PopoverItemDefault, PopoverItemSeparator, PopoverItemType } from './components/popover-item';\nimport Dom from '../../dom';\nimport type { SearchInput } from './components/search-input';\nimport EventsDispatcher from '../events';\nimport Listeners from '../listeners';\nimport type { PopoverEventMap, PopoverMessages, PopoverParams, PopoverNodes } from '@/types/utils/popover/popover';\nimport { PopoverEvent } from '@/types/utils/popover/popover-event';\nimport { css } from './popover.const';\nimport type { PopoverItemParams } from './components/popover-item';\nimport { PopoverItemHtml } from './components/popover-item/popover-item-html/popover-item-html';\n\n/**\n * Class responsible for rendering popover and handling its behaviour\n */\nexport abstract class PopoverAbstract<Nodes extends PopoverNodes = PopoverNodes> extends EventsDispatcher<PopoverEventMap> {\n  /**\n   * List of popover items\n   */\n  protected items: Array<PopoverItem>;\n\n  /**\n   * Listeners util instance\n   */\n  protected listeners: Listeners = new Listeners();\n\n  /**\n   * Refs to created HTML elements\n   */\n  protected nodes: Nodes;\n\n  /**\n   * List of default popover items that are searchable and may have confirmation state\n   */\n  protected get itemsDefault(): PopoverItemDefault[] {\n    return this.items.filter(item => item instanceof PopoverItemDefault) as PopoverItemDefault[];\n  }\n\n  /**\n   * Instance of the Search Input\n   */\n  protected search: SearchInput | undefined;\n\n  /**\n   * Messages that will be displayed in popover\n   */\n  protected messages: PopoverMessages = {\n    nothingFound: 'Nothing found',\n    search: 'Search',\n  };\n\n  /**\n   * Constructs the instance\n   *\n   * @param params - popover construction params\n   * @param itemsRenderParams - popover item render params.\n   * The parameters that are not set by user via popover api but rather depend on technical implementation\n   */\n  constructor(\n    protected readonly params: PopoverParams,\n    protected readonly itemsRenderParams: PopoverItemRenderParamsMap = {}\n  ) {\n    super();\n\n    this.items = this.buildItems(params.items);\n\n    if (params.messages) {\n      this.messages = {\n        ...this.messages,\n        ...params.messages,\n      };\n    }\n\n    /** Build html elements */\n    this.nodes = {} as Nodes;\n\n    this.nodes.popoverContainer = Dom.make('div', [ css.popoverContainer ]);\n\n    this.nodes.nothingFoundMessage = Dom.make('div', [ css.nothingFoundMessage ], {\n      textContent: this.messages.nothingFound,\n    });\n\n    this.nodes.popoverContainer.appendChild(this.nodes.nothingFoundMessage);\n    this.nodes.items = Dom.make('div', [ css.items ]);\n\n    this.items.forEach(item => {\n      const itemEl = item.getElement();\n\n      if (itemEl === null) {\n        return;\n      }\n\n      this.nodes.items.appendChild(itemEl);\n    });\n\n    this.nodes.popoverContainer.appendChild(this.nodes.items);\n\n    this.listeners.on(this.nodes.popoverContainer, 'click', (event: Event) => this.handleClick(event));\n\n    this.nodes.popover = Dom.make('div', [\n      css.popover,\n      this.params.class,\n    ]);\n\n    this.nodes.popover.appendChild(this.nodes.popoverContainer);\n  }\n\n  /**\n   * Returns HTML element corresponding to the popover\n   */\n  public getElement(): HTMLElement {\n    return this.nodes.popover as HTMLElement;\n  }\n\n  /**\n   * Open popover\n   */\n  public show(): void {\n    this.nodes.popover.classList.add(css.popoverOpened);\n\n    if (this.search !== undefined) {\n      this.search.focus();\n    }\n  }\n\n  /**\n   * Closes popover\n   */\n  public hide(): void {\n    this.nodes.popover.classList.remove(css.popoverOpened);\n    this.nodes.popover.classList.remove(css.popoverOpenTop);\n\n    this.itemsDefault.forEach(item => item.reset());\n\n    if (this.search !== undefined) {\n      this.search.clear();\n    }\n\n    this.emit(PopoverEvent.Closed);\n  }\n\n  /**\n   * Clears memory\n   */\n  public destroy(): void {\n    this.items.forEach(item => item.destroy());\n    this.nodes.popover.remove();\n    this.listeners.removeAll();\n    this.search?.destroy();\n  }\n\n  /**\n   * Looks for the item by name and imitates click on it\n   *\n   * @param name - name of the item to activate\n   */\n  public activateItemByName(name: string): void {\n    const foundItem = this.items.find(item => item.name === name);\n\n    this.handleItemClick(foundItem);\n  }\n\n  /**\n   * Factory method for creating popover items\n   *\n   * @param items - list of items params\n   */\n  protected buildItems(items: PopoverItemParams[]): Array<PopoverItem> {\n    return items.map(item => {\n      switch (item.type) {\n        case PopoverItemType.Separator:\n          return new PopoverItemSeparator();\n        case PopoverItemType.Html:\n          return new PopoverItemHtml(item, this.itemsRenderParams[PopoverItemType.Html]);\n        default:\n          return new PopoverItemDefault(item, this.itemsRenderParams[PopoverItemType.Default]);\n      }\n    });\n  }\n\n  /**\n   * Retrieves popover item that is the target of the specified event\n   *\n   * @param event - event to retrieve popover item from\n   */\n  protected getTargetItem(event: Event): PopoverItemDefault | PopoverItemHtml | undefined {\n    return this.items\n      .filter(item => item instanceof PopoverItemDefault || item instanceof PopoverItemHtml)\n      .find(item => {\n        const itemEl = item.getElement();\n\n        if (itemEl === null) {\n          return false;\n        }\n\n        return event.composedPath().includes(itemEl);\n      }) as PopoverItemDefault | PopoverItemHtml | undefined;\n  }\n\n  /**\n   * Handles popover item click\n   *\n   * @param item - item to handle click of\n   */\n  protected handleItemClick(item: PopoverItem): void {\n    if ('isDisabled' in item && item.isDisabled) {\n      return;\n    }\n\n    if (item.hasChildren) {\n      this.showNestedItems(item as PopoverItemDefault | PopoverItemHtml);\n\n      if ('handleClick' in item && typeof item.handleClick === 'function') {\n        item.handleClick();\n      }\n\n      return;\n    }\n\n    /** Cleanup other items state */\n    this.itemsDefault.filter(x => x !== item).forEach(x => x.reset());\n\n    if ('handleClick' in item && typeof item.handleClick === 'function') {\n      item.handleClick();\n    }\n\n    this.toggleItemActivenessIfNeeded(item);\n\n    if (item.closeOnActivate) {\n      this.hide();\n\n      this.emit(PopoverEvent.ClosedOnActivate);\n    }\n  }\n\n  /**\n   * Handles clicks inside popover\n   *\n   * @param event - item to handle click of\n   */\n  private handleClick(event: Event): void {\n    const item = this.getTargetItem(event);\n\n    if (item === undefined) {\n      return;\n    }\n\n    this.handleItemClick(item);\n  }\n\n  /**\n   * - Toggles item active state, if clicked popover item has property 'toggle' set to true.\n   *\n   * - Performs radiobutton-like behavior if the item has property 'toggle' set to string key.\n   * (All the other items with the same key get inactive, and the item gets active)\n   *\n   * @param clickedItem - popover item that was clicked\n   */\n  private toggleItemActivenessIfNeeded(clickedItem: PopoverItem): void {\n    if (!(clickedItem instanceof PopoverItemDefault)) {\n      return;\n    }\n\n    if (clickedItem.toggle === true) {\n      clickedItem.toggleActive();\n    }\n\n    if (typeof clickedItem.toggle === 'string') {\n      const itemsInToggleGroup = this.itemsDefault.filter(item => item.toggle === clickedItem.toggle);\n\n      /** If there's only one item in toggle group, toggle it */\n      if (itemsInToggleGroup.length === 1) {\n        clickedItem.toggleActive();\n\n        return;\n      }\n\n      /** Set clicked item as active and the rest items with same toggle key value as inactive */\n      itemsInToggleGroup.forEach(item => {\n        item.toggleActive(item === clickedItem);\n      });\n    }\n  }\n\n  /**\n   * Handles displaying nested items for the item. Behaviour differs depending on platform.\n   *\n   * @param item – item to show nested popover for\n   */\n  protected abstract showNestedItems(item: PopoverItemDefault | PopoverItemHtml): void;\n}\n"
  },
  {
    "path": "src/components/utils/popover/popover-desktop.ts",
    "content": "import Flipper from '../../flipper';\nimport { PopoverAbstract } from './popover-abstract';\nimport type { PopoverItem, PopoverItemRenderParamsMap } from './components/popover-item';\nimport { PopoverItemSeparator, css as popoverItemCls } from './components/popover-item';\nimport type { PopoverParams } from '@/types/utils/popover/popover';\nimport { PopoverEvent } from '@/types/utils/popover/popover-event';\nimport { keyCodes } from '../../utils';\nimport { CSSVariables, css } from './popover.const';\nimport type { SearchableItem } from './components/search-input';\nimport { SearchInput, SearchInputEvent } from './components/search-input';\nimport { cacheable } from '../../utils';\nimport { PopoverItemDefault } from './components/popover-item';\nimport { PopoverItemHtml } from './components/popover-item/popover-item-html/popover-item-html';\n\n/**\n * Desktop popover.\n * On desktop devices popover behaves like a floating element. Nested popover appears at right or left side.\n *\n * @todo support rtl for nested popovers and search\n */\nexport class PopoverDesktop extends PopoverAbstract {\n  /**\n   * Flipper - module for keyboard iteration between elements\n   */\n  public flipper: Flipper | undefined;\n\n  /**\n   * Popover nesting level. 0 value means that it is a root popover\n   */\n  public nestingLevel = 0;\n\n  /**\n   * Reference to nested popover if exists.\n   * Undefined by default, PopoverDesktop when exists and null after destroyed.\n   */\n  protected nestedPopover: PopoverDesktop | undefined | null;\n\n  /**\n   * Item nested popover is displayed for\n   */\n  protected nestedPopoverTriggerItem: PopoverItem | null = null;\n\n  /**\n   * Last hovered item inside popover.\n   * Is used to determine if cursor is moving inside one item or already moved away to another one.\n   * Helps prevent reopening nested popover while cursor is moving inside one item area.\n   */\n  private previouslyHoveredItem: PopoverItem | null = null;\n\n  /**\n   * Element of the page that creates 'scope' of the popover.\n   * If possible, popover will not cross specified element's borders when opening.\n   */\n  private scopeElement: HTMLElement = document.body;\n\n  /**\n   * Construct the instance\n   *\n   * @param params - popover params\n   * @param itemsRenderParams – popover item render params.\n   * The parameters that are not set by user via popover api but rather depend on technical implementation\n   */\n  constructor(params: PopoverParams, itemsRenderParams?: PopoverItemRenderParamsMap) {\n    super(params, itemsRenderParams);\n\n    if (params.nestingLevel !== undefined) {\n      this.nestingLevel = params.nestingLevel;\n    }\n\n    if (this.nestingLevel > 0) {\n      this.nodes.popover.classList.add(css.popoverNested);\n    }\n\n    if (params.scopeElement !== undefined) {\n      this.scopeElement = params.scopeElement;\n    }\n\n    if (this.nodes.popoverContainer !== null) {\n      this.listeners.on(this.nodes.popoverContainer, 'mouseover', (event: Event) => this.handleHover(event));\n    }\n\n    if (params.searchable) {\n      this.addSearch();\n    }\n\n    if (params.flippable !== false) {\n      this.flipper = new Flipper({\n        items: this.flippableElements,\n        focusedItemClass: popoverItemCls.focused,\n        allowedKeys: [\n          keyCodes.TAB,\n          keyCodes.UP,\n          keyCodes.DOWN,\n          keyCodes.ENTER,\n        ],\n      });\n\n      this.flipper.onFlip(this.onFlip);\n    }\n  }\n\n  /**\n   * Returns true if some item inside popover is focused\n   */\n  public hasFocus(): boolean {\n    if (this.flipper === undefined) {\n      return false;\n    }\n\n    return this.flipper.hasFocus();\n  }\n\n  /**\n   * Scroll position inside items container of the popover\n   */\n  public get scrollTop(): number {\n    if (this.nodes.items === null) {\n      return 0;\n    }\n\n    return this.nodes.items.scrollTop;\n  }\n\n  /**\n   * Returns visible element offset top\n   */\n  public get offsetTop(): number {\n    if (this.nodes.popoverContainer === null) {\n      return 0;\n    }\n\n    return this.nodes.popoverContainer.offsetTop;\n  }\n\n  /**\n   * Open popover\n   */\n  public show(): void {\n    this.nodes.popover.style.setProperty(CSSVariables.PopoverHeight, this.size.height + 'px');\n\n    if (!this.shouldOpenBottom) {\n      this.nodes.popover.classList.add(css.popoverOpenTop);\n    }\n\n    if (!this.shouldOpenRight) {\n      this.nodes.popover.classList.add(css.popoverOpenLeft);\n    }\n\n    super.show();\n    this.flipper?.activate(this.flippableElements);\n  }\n\n  /**\n   * Closes popover\n   */\n  public hide = (): void => {\n    super.hide();\n\n    this.destroyNestedPopoverIfExists();\n\n    this.flipper?.deactivate();\n\n    this.previouslyHoveredItem = null;\n  };\n\n  /**\n   * Clears memory\n   */\n  public destroy(): void {\n    this.hide();\n    super.destroy();\n  }\n\n  /**\n   * Handles displaying nested items for the item.\n   *\n   * @param item – item to show nested popover for\n   */\n  protected override showNestedItems(item: PopoverItem): void {\n    if (this.nestedPopover !== null && this.nestedPopover !== undefined) {\n      return;\n    }\n\n    this.nestedPopoverTriggerItem = item;\n\n    this.showNestedPopoverForItem(item);\n  }\n\n  /**\n   * Handles hover events inside popover items container\n   *\n   * @param event - hover event data\n   */\n  protected handleHover(event: Event): void {\n    const item = this.getTargetItem(event);\n\n    if (item === undefined) {\n      return;\n    }\n\n    if (this.previouslyHoveredItem === item) {\n      return;\n    }\n\n    this.destroyNestedPopoverIfExists();\n\n    this.previouslyHoveredItem = item;\n\n    if (!item.hasChildren) {\n      return;\n    }\n\n    this.showNestedPopoverForItem(item);\n  }\n\n  /**\n   * Sets CSS variable with position of item near which nested popover should be displayed.\n   * Is used for correct positioning of the nested popover\n   *\n   * @param nestedPopoverEl - nested popover element\n   * @param item – item near which nested popover should be displayed\n   */\n  protected setTriggerItemPosition(nestedPopoverEl: HTMLElement, item: PopoverItem): void {\n    const itemEl = item.getElement();\n    const itemOffsetTop = (itemEl ? itemEl.offsetTop : 0) - this.scrollTop;\n    const topOffset = this.offsetTop + itemOffsetTop;\n\n    nestedPopoverEl.style.setProperty(CSSVariables.TriggerItemTop, topOffset + 'px');\n  }\n\n  /**\n   * Destroys existing nested popover\n   */\n  protected destroyNestedPopoverIfExists(): void {\n    if (this.nestedPopover === undefined || this.nestedPopover === null) {\n      return;\n    }\n\n    this.nestedPopover.off(PopoverEvent.ClosedOnActivate, this.hide);\n    this.nestedPopover.hide();\n    this.nestedPopover.destroy();\n    this.nestedPopover.getElement().remove();\n    this.nestedPopover = null;\n    this.flipper?.activate(this.flippableElements);\n\n    this.nestedPopoverTriggerItem?.onChildrenClose();\n  }\n\n  /**\n   * Creates and displays nested popover for specified item.\n   * Is used only on desktop\n   *\n   * @param item - item to display nested popover by\n   */\n  protected showNestedPopoverForItem(item: PopoverItem): PopoverDesktop {\n    this.nestedPopover = new PopoverDesktop({\n      searchable: item.isChildrenSearchable,\n      items: item.children,\n      nestingLevel: this.nestingLevel + 1,\n      flippable: item.isChildrenFlippable,\n      messages: this.messages,\n    });\n\n    item.onChildrenOpen();\n\n    /**\n     * Close nested popover when item with 'closeOnActivate' property set was clicked\n     * parent popover should also be closed\n     */\n    this.nestedPopover.on(PopoverEvent.ClosedOnActivate, this.hide);\n\n    const nestedPopoverEl = this.nestedPopover.getElement();\n\n    this.nodes.popover.appendChild(nestedPopoverEl);\n\n    this.setTriggerItemPosition(nestedPopoverEl, item);\n\n    /* We need nesting level value in CSS to calculate offset left for nested popover */\n    nestedPopoverEl.style.setProperty(CSSVariables.NestingLevel, this.nestedPopover.nestingLevel.toString());\n\n    this.nestedPopover.show();\n    this.flipper?.deactivate();\n\n    return this.nestedPopover;\n  }\n\n  /**\n   * Checks if popover should be opened bottom.\n   * It should happen when there is enough space below or not enough space above\n   */\n  private get shouldOpenBottom(): boolean {\n    if (this.nodes.popover === undefined || this.nodes.popover === null) {\n      return false;\n    }\n    const popoverRect = this.nodes.popoverContainer.getBoundingClientRect();\n    const scopeElementRect = this.scopeElement.getBoundingClientRect();\n    const popoverHeight = this.size.height;\n    const popoverPotentialBottomEdge = popoverRect.top + popoverHeight;\n    const popoverPotentialTopEdge = popoverRect.top - popoverHeight;\n    const bottomEdgeForComparison = Math.min(window.innerHeight, scopeElementRect.bottom);\n\n    return popoverPotentialTopEdge < scopeElementRect.top || popoverPotentialBottomEdge <= bottomEdgeForComparison;\n  }\n\n  /**\n   * Checks if popover should be opened left.\n   * It should happen when there is enough space in the right or not enough space in the left\n   */\n  private get shouldOpenRight(): boolean {\n    if (this.nodes.popover === undefined || this.nodes.popover === null) {\n      return false;\n    }\n\n    const popoverRect = this.nodes.popover.getBoundingClientRect();\n    const scopeElementRect = this.scopeElement.getBoundingClientRect();\n    const popoverWidth = this.size.width;\n    const popoverPotentialRightEdge = popoverRect.right + popoverWidth;\n    const popoverPotentialLeftEdge = popoverRect.left - popoverWidth;\n    const rightEdgeForComparison = Math.min(window.innerWidth, scopeElementRect.right);\n\n    return popoverPotentialLeftEdge < scopeElementRect.left || popoverPotentialRightEdge <= rightEdgeForComparison;\n  }\n\n  /**\n   * Helps to calculate size of popover that is only resolved when popover is displayed on screen.\n   * Renders invisible clone of popover to get actual values.\n   */\n  @cacheable\n  public get size(): { height: number; width: number } {\n    const size = {\n      height: 0,\n      width: 0,\n    };\n\n    if (this.nodes.popover === null) {\n      return size;\n    }\n\n    const popoverClone = this.nodes.popover.cloneNode(true) as HTMLElement;\n\n    popoverClone.style.visibility = 'hidden';\n    popoverClone.style.position = 'absolute';\n    popoverClone.style.top = '-1000px';\n\n    popoverClone.classList.add(css.popoverOpened);\n    popoverClone.querySelector('.' + css.popoverNested)?.remove();\n    document.body.appendChild(popoverClone);\n\n    const container = popoverClone.querySelector('.' + css.popoverContainer) as HTMLElement;\n\n    size.height = container.offsetHeight;\n    size.width = container.offsetWidth;\n    popoverClone.remove();\n\n    return size;\n  }\n\n  /**\n   * Returns list of elements available for keyboard navigation.\n   */\n  private get flippableElements(): HTMLElement[] {\n    const result = this.items\n      .map(item => {\n        if (item instanceof PopoverItemDefault) {\n          return item.getElement();\n        }\n        if (item instanceof PopoverItemHtml) {\n          return item.getControls();\n        }\n      })\n      .flat()\n      .filter(item => item !== undefined && item !== null);\n\n    return result as HTMLElement[];\n  }\n\n  /**\n   * Called on flipper navigation\n   */\n  private onFlip = (): void => {\n    const focusedItem = this.itemsDefault.find(item => item.isFocused);\n\n    focusedItem?.onFocus();\n  };\n\n  /**\n   * Adds search to the popover\n   */\n  private addSearch(): void {\n    this.search = new SearchInput({\n      items: this.itemsDefault,\n      placeholder: this.messages.search,\n    });\n\n    this.search.on(SearchInputEvent.Search, this.onSearch);\n\n    const searchElement = this.search.getElement();\n\n    searchElement.classList.add(css.search);\n\n    this.nodes.popoverContainer.insertBefore(searchElement, this.nodes.popoverContainer.firstChild);\n  }\n\n  /**\n   * Handles input inside search field\n   *\n   * @param data - search input event data\n   * @param data.query - search query text\n   * @param data.result - search results\n   */\n  private onSearch = (data: { query: string, items: SearchableItem[] }): void => {\n    const isEmptyQuery = data.query === '';\n    const isNothingFound = data.items.length === 0;\n\n    this.items\n      .forEach((item) => {\n        let isHidden = false;\n\n        if (item instanceof PopoverItemDefault) {\n          isHidden = !data.items.includes(item);\n        } else if (item instanceof PopoverItemSeparator || item instanceof PopoverItemHtml) {\n          /** Should hide separators if nothing found message displayed or if there is some search query applied */\n          isHidden = isNothingFound || !isEmptyQuery;\n        }\n        item.toggleHidden(isHidden);\n      });\n    this.toggleNothingFoundMessage(isNothingFound);\n\n    /** List of elements available for keyboard navigation considering search query applied */\n    const flippableElements = data.query === '' ? this.flippableElements : data.items.map(item => (item as PopoverItem).getElement());\n\n    if (this.flipper?.isActivated) {\n      /** Update flipper items with only visible */\n      this.flipper.deactivate();\n      this.flipper.activate(flippableElements as HTMLElement[]);\n    }\n  };\n\n  /**\n   * Toggles nothing found message visibility\n   *\n   * @param isDisplayed - true if the message should be displayed\n   */\n  private toggleNothingFoundMessage(isDisplayed: boolean): void {\n    this.nodes.nothingFoundMessage.classList.toggle(css.nothingFoundMessageDisplayed, isDisplayed);\n  }\n}\n"
  },
  {
    "path": "src/components/utils/popover/popover-inline.ts",
    "content": "import { isMobileScreen } from '../../utils';\nimport type { PopoverItem } from './components/popover-item';\nimport { PopoverItemDefault, PopoverItemType } from './components/popover-item';\nimport { PopoverItemHtml } from './components/popover-item/popover-item-html/popover-item-html';\nimport { PopoverDesktop } from './popover-desktop';\nimport { CSSVariables, css } from './popover.const';\nimport type { PopoverParams } from '@/types/utils/popover/popover';\n\n/**\n * Horizontal popover that is displayed inline with the content\n */\nexport class PopoverInline extends PopoverDesktop {\n  /**\n   * Constructs the instance\n   *\n   * @param params - instance parameters\n   */\n  constructor(params: PopoverParams) {\n    const isHintEnabled = !isMobileScreen();\n\n    super(\n      {\n        ...params,\n        class: css.popoverInline,\n      },\n      {\n        [PopoverItemType.Default]: {\n          /**\n           * We use button instead of div here to fix bug associated with focus loss (which leads to selection change) on click in safari\n           *\n           * @todo figure out better way to solve the issue\n           */\n          wrapperTag: 'button',\n          hint: {\n            position: 'top',\n            alignment: 'center',\n            enabled: isHintEnabled,\n          },\n        },\n        [PopoverItemType.Html]: {\n          hint: {\n            position: 'top',\n            alignment: 'center',\n            enabled: isHintEnabled,\n          },\n        },\n      }\n    );\n\n    /**\n     * If active popover item has children, show them.\n     * This is needed to display link url text (which is displayed as a nested popover content)\n     * once you select <a> tag content in text\n     */\n    this.items\n      .forEach((item) => {\n        if (!(item instanceof PopoverItemDefault) && !(item instanceof PopoverItemHtml)) {\n          return;\n        }\n\n        if (item.hasChildren && item.isChildrenOpen) {\n          this.showNestedItems(item);\n        }\n      });\n  }\n\n  /**\n   * Returns visible element offset top\n   */\n  public get offsetLeft(): number {\n    if (this.nodes.popoverContainer === null) {\n      return 0;\n    }\n\n    return this.nodes.popoverContainer.offsetLeft;\n  }\n\n  /**\n   * Open popover\n   */\n  public override show(): void {\n    /**\n     * If this is not a nested popover, set CSS variable with width of the popover\n     */\n    if (this.nestingLevel === 0) {\n      this.nodes.popover.style.setProperty(\n        CSSVariables.InlinePopoverWidth,\n        this.size.width + 'px'\n      );\n    }\n    super.show();\n  }\n\n  /**\n   * Disable hover event handling.\n   * Overrides parent's class behavior\n   */\n  protected override handleHover(): void {\n    return;\n  }\n\n  /**\n   * Sets CSS variable with position of item near which nested popover should be displayed.\n   * Is used to position nested popover right below clicked item\n   *\n   * @param nestedPopoverEl - nested popover element\n   * @param item – item near which nested popover should be displayed\n   */\n  protected override setTriggerItemPosition(\n    nestedPopoverEl: HTMLElement,\n    item: PopoverItemDefault\n  ): void {\n    const itemEl = item.getElement();\n    const itemOffsetLeft = itemEl ? itemEl.offsetLeft : 0;\n    const totalLeftOffset = this.offsetLeft + itemOffsetLeft;\n\n    nestedPopoverEl.style.setProperty(\n      CSSVariables.TriggerItemLeft,\n      totalLeftOffset + 'px'\n    );\n  }\n\n  /**\n   * Handles displaying nested items for the item.\n   * Overriding in order to add toggling behaviour\n   *\n   * @param item – item to toggle nested popover for\n   */\n  protected override showNestedItems(item: PopoverItemDefault | PopoverItemHtml): void {\n    if (this.nestedPopoverTriggerItem === item) {\n      this.destroyNestedPopoverIfExists();\n\n      this.nestedPopoverTriggerItem = null;\n\n      return;\n    }\n\n    super.showNestedItems(item);\n  }\n\n  /**\n   * Creates and displays nested popover for specified item.\n   * Is used only on desktop\n   *\n   * @param item - item to display nested popover by\n   */\n  protected showNestedPopoverForItem(item: PopoverItem): PopoverDesktop {\n    const nestedPopover = super.showNestedPopoverForItem(item);\n    const nestedPopoverEl = nestedPopover.getElement();\n\n    /**\n     * We need to add class with nesting level, shich will help position nested popover.\n     * Currently only '.ce-popover--nested-level-1' class is used\n     */\n    nestedPopoverEl.classList.add(css.getPopoverNestedClass(nestedPopover.nestingLevel));\n\n    return nestedPopover;\n  }\n\n  /**\n   * Overrides default item click handling.\n   * Helps to close nested popover once other item is clicked.\n   *\n   * @param item - clicked item\n   */\n  protected override handleItemClick(item: PopoverItem): void {\n    if (item !== this.nestedPopoverTriggerItem) {\n      /**\n       * In case tool had special handling for toggling button (like link tool which modifies selection)\n       * we need to call handleClick on nested popover trigger item\n       */\n      this.nestedPopoverTriggerItem?.handleClick();\n\n      /**\n       * Then close the nested popover\n       */\n      super.destroyNestedPopoverIfExists();\n    }\n\n    super.handleItemClick(item);\n  }\n}\n"
  },
  {
    "path": "src/components/utils/popover/popover-mobile.ts",
    "content": "import { PopoverAbstract } from './popover-abstract';\nimport ScrollLocker from '../scroll-locker';\nimport { PopoverHeader } from './components/popover-header';\nimport { PopoverStatesHistory } from './utils/popover-states-history';\nimport type { PopoverMobileNodes, PopoverParams } from '@/types/utils/popover/popover';\nimport type { PopoverItemDefault, PopoverItemParams } from './components/popover-item';\nimport { PopoverItemType } from './components/popover-item';\nimport { css } from './popover.const';\nimport Dom from '../../dom';\n\n\n/**\n * Mobile Popover.\n * On mobile devices Popover behaves like a fixed panel at the bottom of screen. Nested item appears like \"pages\" with the \"back\" button\n */\nexport class PopoverMobile extends PopoverAbstract<PopoverMobileNodes> {\n  /**\n   * ScrollLocker instance\n   */\n  private scrollLocker = new ScrollLocker();\n\n  /**\n   * Reference to popover header if exists\n   */\n  private header: PopoverHeader | undefined | null;\n\n  /**\n   * History of popover states for back navigation.\n   * Is used for mobile version of popover,\n   * where we can not display nested popover of the screen and\n   * have to render nested items in the same popover switching to new state\n   */\n  private history = new PopoverStatesHistory();\n\n  /**\n   * Flag that indicates if popover is hidden\n   */\n  private isHidden = true;\n\n  /**\n   * Construct the instance\n   *\n   * @param params - popover params\n   */\n  constructor(params: PopoverParams) {\n    super(params, {\n      [PopoverItemType.Default]: {\n        hint: {\n          enabled: false,\n        },\n      },\n      [PopoverItemType.Html]: {\n        hint: {\n          enabled: false,\n        },\n      },\n    });\n\n    this.nodes.overlay = Dom.make('div', [css.overlay, css.overlayHidden]);\n    this.nodes.popover.insertBefore(this.nodes.overlay, this.nodes.popover.firstChild);\n\n    this.listeners.on(this.nodes.overlay, 'click', () => {\n      this.hide();\n    });\n\n    /* Save state to history for proper navigation between nested and parent popovers */\n    this.history.push({ items: params.items });\n  }\n\n  /**\n   * Open popover\n   */\n  public show(): void {\n    this.nodes.overlay.classList.remove(css.overlayHidden);\n\n    super.show();\n\n    this.scrollLocker.lock();\n\n    this.isHidden = false;\n  }\n\n  /**\n   * Closes popover\n   */\n  public hide(): void {\n    if (this.isHidden) {\n      return;\n    }\n\n    super.hide();\n    this.nodes.overlay.classList.add(css.overlayHidden);\n\n    this.scrollLocker.unlock();\n\n    this.history.reset();\n\n    this.isHidden = true;\n  }\n\n  /**\n   * Clears memory\n   */\n  public destroy(): void {\n    super.destroy();\n\n    this.scrollLocker.unlock();\n  }\n\n  /**\n   * Handles displaying nested items for the item\n   *\n   * @param item – item to show nested popover for\n   */\n  protected override showNestedItems(item: PopoverItemDefault): void {\n    /** Show nested items */\n    this.updateItemsAndHeader(item.children, item.title);\n\n    this.history.push({\n      title: item.title,\n      items: item.children,\n    });\n  }\n\n  /**\n   * Removes rendered popover items and header and displays new ones\n   *\n   * @param items - new popover items\n   * @param title - new popover header text\n   */\n  private updateItemsAndHeader(items: PopoverItemParams[], title?: string ): void {\n    /** Re-render header */\n    if (this.header !== null && this.header !== undefined) {\n      this.header.destroy();\n      this.header = null;\n    }\n    if (title !== undefined) {\n      this.header = new PopoverHeader({\n        text: title,\n        onBackButtonClick: () => {\n          this.history.pop();\n\n          this.updateItemsAndHeader(this.history.currentItems, this.history.currentTitle);\n        },\n      });\n      const headerEl = this.header.getElement();\n\n      if (headerEl !== null) {\n        this.nodes.popoverContainer.insertBefore(headerEl, this.nodes.popoverContainer.firstChild);\n      }\n    }\n\n    /** Re-render items */\n    this.items.forEach(item => item.getElement()?.remove());\n\n    this.items = this.buildItems(items);\n\n    this.items.forEach(item => {\n      const itemEl = item.getElement();\n\n      if (itemEl === null) {\n        return;\n      }\n      this.nodes.items?.appendChild(itemEl);\n    });\n  }\n}\n"
  },
  {
    "path": "src/components/utils/popover/popover.const.ts",
    "content": "import { bem } from '../bem';\n\n/**\n * Popover block CSS class constructor\n */\nconst className = bem('ce-popover');\n\n/**\n * CSS class names to be used in popover\n */\nexport const css = {\n  popover: className(),\n  popoverContainer: className('container'),\n  popoverOpenTop: className(null, 'open-top'),\n  popoverOpenLeft: className(null, 'open-left'),\n  popoverOpened: className(null, 'opened'),\n  search: className('search'),\n  nothingFoundMessage: className('nothing-found-message'),\n  nothingFoundMessageDisplayed: className('nothing-found-message', 'displayed'),\n  items: className('items'),\n  overlay: className('overlay'),\n  overlayHidden: className('overlay', 'hidden'),\n  popoverNested: className(null, 'nested'),\n  getPopoverNestedClass: (level: number) => className(null, `nested-level-${level.toString()}` ),\n  popoverInline: className(null, 'inline'),\n  popoverHeader: className('header'),\n};\n\n/**\n * CSS variables names to be used in popover\n */\nexport enum CSSVariables {\n  /**\n   * Stores nesting level of the popover\n   */\n  NestingLevel = '--nesting-level',\n\n  /**\n   * Stores actual popover height. Used for desktop popovers\n   */\n  PopoverHeight = '--popover-height',\n\n  /**\n   * Width of the inline popover\n   */\n  InlinePopoverWidth = '--inline-popover-width',\n\n  /**\n   * Offset from left of the inline popover item click on which triggers the nested popover opening\n   */\n  TriggerItemLeft = '--trigger-item-left',\n\n  /**\n   * Offset from top of the desktop popover item click on which triggers the nested popover opening\n   */\n  TriggerItemTop = '--trigger-item-top',\n}\n"
  },
  {
    "path": "src/components/utils/popover/utils/popover-states-history.ts",
    "content": "import type { PopoverItemParams } from '@/types/utils/popover/popover-item';\n\n/**\n * Represents single states history item\n */\ninterface PopoverStatesHistoryItem {\n  /**\n   * Popover title\n   */\n  title?: string;\n\n  /**\n   * Popover items\n   */\n  items: PopoverItemParams[]\n}\n\n/**\n * Manages items history inside popover. Allows to navigate back in history\n */\nexport class PopoverStatesHistory {\n  /**\n   * Previous items states\n   */\n  private history: PopoverStatesHistoryItem[] = [];\n\n  /**\n   * Push new popover state\n   *\n   * @param state - new state\n   */\n  public push(state: PopoverStatesHistoryItem): void {\n    this.history.push(state);\n  }\n\n  /**\n   * Pop last popover state\n   */\n  public pop(): PopoverStatesHistoryItem | undefined {\n    return this.history.pop();\n  }\n\n  /**\n   * Title retrieved from the current state\n   */\n  public get currentTitle(): string | undefined {\n    if (this.history.length === 0) {\n      return '';\n    }\n\n    return this.history[this.history.length - 1].title;\n  }\n\n  /**\n   * Items list retrieved from the current state\n   */\n  public get currentItems(): PopoverItemParams[] {\n    if (this.history.length === 0) {\n      return [];\n    }\n\n    return this.history[this.history.length - 1].items;\n  }\n\n  /**\n   * Returns history to initial popover state\n   */\n  public reset(): void  {\n    while (this.history.length > 1) {\n      this.pop();\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/utils/promise-queue.ts",
    "content": "/**\n * Class allows to make a queue of async jobs and wait until they all will be finished one by one\n *\n * @example const queue = new PromiseQueue();\n *            queue.add(async () => { ... });\n *            queue.add(async () => { ... });\n *            await queue.completed;\n */\nexport default class PromiseQueue {\n  /**\n   * Queue of promises to be executed\n   */\n  public completed = Promise.resolve();\n\n  /**\n   * Add new promise to queue\n   *\n   * @param operation - promise should be added to queue\n   */\n  public add(operation: (value: void) => void | PromiseLike<void>): Promise<void> {\n    return new Promise((resolve, reject) => {\n      this.completed = this.completed\n        .then(operation)\n        .then(resolve)\n        .catch(reject);\n    });\n  }\n}\n"
  },
  {
    "path": "src/components/utils/resolve-aliases.ts",
    "content": "/**\n * Resolves aliases in specified object according to passed aliases info\n *\n * @example resolveAliases(obj, { label: 'title' })\n * here 'label' is alias for 'title'\n * @param obj - object with aliases to be resolved\n * @param aliases - object with aliases info where key is an alias property name and value is an aliased property name\n */\nexport function resolveAliases<ObjectType>(obj: ObjectType, aliases: { [alias: string]: string }): ObjectType {\n  const result = {} as ObjectType;\n\n  Object.keys(obj).forEach(property => {\n    const aliasedProperty = aliases[property];\n\n    if (aliasedProperty !== undefined) {\n      result[aliasedProperty] = obj[property];\n    } else {\n      result[property] = obj[property];\n    }\n  });\n\n  return result;\n}\n"
  },
  {
    "path": "src/components/utils/sanitizer.ts",
    "content": "/* eslint-disable @typescript-eslint/no-use-before-define */\n/**\n * CodeX Sanitizer\n *\n * Clears HTML from taint tags\n *\n * @version 2.0.0\n * @example\n *\n * clean(yourTaintString, yourConfig);\n *\n * {@link SanitizerConfig}\n */\n\nimport * as _ from '../utils';\n\n/**\n * @typedef {object} SanitizerConfig\n * @property {object} tags - define tags restrictions\n * @example\n *\n * tags : {\n *     p: true,\n *     a: {\n *       href: true,\n *       rel: \"nofollow\",\n *       target: \"_blank\"\n *     }\n * }\n */\n\nimport HTMLJanitor from 'html-janitor';\nimport type { BlockToolData, SanitizerConfig } from '../../../types';\nimport type { SavedData } from '../../../types/data-formats';\n\n/**\n * Sanitize Blocks\n *\n * Enumerate blocks and clean data\n *\n * @param blocksData - blocks' data to sanitize\n * @param sanitizeConfig — sanitize config to use or function to get config for Tool\n */\nexport function sanitizeBlocks(\n  blocksData: Array<Pick<SavedData, 'data' | 'tool'>>,\n  sanitizeConfig: SanitizerConfig | ((toolName: string) => SanitizerConfig)\n): Array<Pick<SavedData, 'data' | 'tool'>> {\n  return blocksData.map((block) => {\n    const toolConfig = _.isFunction(sanitizeConfig) ? sanitizeConfig(block.tool) : sanitizeConfig;\n\n    if (_.isEmpty(toolConfig)) {\n      return block;\n    }\n\n    block.data = deepSanitize(block.data, toolConfig) as BlockToolData;\n\n    return block;\n  });\n}\n/**\n * Cleans string from unwanted tags\n * Method allows to use default config\n *\n * @param {string} taintString - taint string\n * @param {SanitizerConfig} customConfig - allowed tags\n * @returns {string} clean HTML\n */\nexport function clean(taintString: string, customConfig: SanitizerConfig = {} as SanitizerConfig): string {\n  const sanitizerConfig = {\n    tags: customConfig,\n  };\n\n  /**\n   * API client can use custom config to manage sanitize process\n   */\n  const sanitizerInstance = new HTMLJanitor(sanitizerConfig);\n\n  return sanitizerInstance.clean(taintString);\n}\n\n/**\n * Method recursively reduces Block's data and cleans with passed rules\n *\n * @param {BlockToolData|object|*} dataToSanitize - taint string or object/array that contains taint string\n * @param {SanitizerConfig} rules - object with sanitizer rules\n */\nfunction deepSanitize(dataToSanitize: object | string, rules: SanitizerConfig): object | string {\n  /**\n   * BlockData It may contain 3 types:\n   *  - Array\n   *  - Object\n   *  - Primitive\n   */\n  if (Array.isArray(dataToSanitize)) {\n    /**\n     * Array: call sanitize for each item\n     */\n    return cleanArray(dataToSanitize, rules);\n  } else if (_.isObject(dataToSanitize)) {\n    /**\n     * Objects: just clean object deeper.\n     */\n    return cleanObject(dataToSanitize, rules);\n  } else {\n    /**\n     * Primitives (number|string|boolean): clean this item\n     *\n     * Clean only strings\n     */\n    if (_.isString(dataToSanitize)) {\n      return cleanOneItem(dataToSanitize, rules);\n    }\n\n    return dataToSanitize;\n  }\n}\n\n/**\n * Clean array\n *\n * @param {Array} array - [1, 2, {}, []]\n * @param {SanitizerConfig} ruleForItem - sanitizer config for array\n */\nfunction cleanArray(array: Array<object | string>, ruleForItem: SanitizerConfig): Array<object | string> {\n  return array.map((arrayItem) => deepSanitize(arrayItem, ruleForItem));\n}\n\n/**\n * Clean object\n *\n * @param {object} object  - {level: 0, text: 'adada', items: [1,2,3]}}\n * @param {object} rules - { b: true } or true|false\n * @returns {object}\n */\nfunction cleanObject(object: object, rules: SanitizerConfig|{[field: string]: SanitizerConfig}): object {\n  const cleanData = {};\n\n  for (const fieldName in object) {\n    if (!Object.prototype.hasOwnProperty.call(object, fieldName)) {\n      continue;\n    }\n\n    const currentIterationItem = object[fieldName];\n\n    /**\n     *  Get object from config by field name\n     *   - if it is a HTML Janitor rule, call with this rule\n     *   - otherwise, call with parent's config\n     */\n    const ruleForItem = isRule(rules[fieldName] as SanitizerConfig) ? rules[fieldName] : rules;\n\n    cleanData[fieldName] = deepSanitize(currentIterationItem, ruleForItem as SanitizerConfig);\n  }\n\n  return cleanData;\n}\n\n/**\n * Clean primitive value\n *\n * @param {string} taintString - string to clean\n * @param {SanitizerConfig|boolean} rule - sanitizer rule\n * @returns {string}\n */\nfunction cleanOneItem(taintString: string, rule: SanitizerConfig|boolean): string {\n  if (_.isObject(rule)) {\n    return clean(taintString, rule);\n  } else if (rule === false) {\n    return clean(taintString, {} as SanitizerConfig);\n  } else {\n    return taintString;\n  }\n}\n\n/**\n * Check if passed item is a HTML Janitor rule:\n *  { a : true }, {}, false, true, function(){} — correct rules\n *  undefined, null, 0, 1, 2 — not a rules\n *\n * @param {SanitizerConfig} config - config to check\n */\nfunction isRule(config: SanitizerConfig): boolean {\n  return _.isObject(config) || _.isBoolean(config) || _.isFunction(config);\n}\n"
  },
  {
    "path": "src/components/utils/scroll-locker.ts",
    "content": "import { isIosDevice } from '../utils';\n\n/**\n * Utility allowing to lock body scroll on demand\n */\nexport default class ScrollLocker {\n  /**\n   * Style classes\n   */\n  private static CSS = {\n    scrollLocked: 'ce-scroll-locked',\n    scrollLockedHard: 'ce-scroll-locked--hard',\n  };\n\n  /**\n   * Stores scroll position, used for hard scroll lock\n   */\n  private scrollPosition: null | number = null;\n\n  /**\n   * Locks body element scroll\n   */\n  public lock(): void {\n    if (isIosDevice) {\n      this.lockHard();\n    } else {\n      document.body.classList.add(ScrollLocker.CSS.scrollLocked);\n    }\n  }\n\n  /**\n   * Unlocks body element scroll\n   */\n  public unlock(): void {\n    if (isIosDevice) {\n      this.unlockHard();\n    } else {\n      document.body.classList.remove(ScrollLocker.CSS.scrollLocked);\n    }\n  }\n\n  /**\n   * Locks scroll in a hard way (via setting fixed position to body element)\n   */\n  private lockHard(): void {\n    this.scrollPosition = window.pageYOffset;\n    document.documentElement.style.setProperty(\n      '--window-scroll-offset',\n      `${this.scrollPosition}px`\n    );\n    document.body.classList.add(ScrollLocker.CSS.scrollLockedHard);\n  }\n\n  /**\n   * Unlocks hard scroll lock\n   */\n  private unlockHard(): void {\n    document.body.classList.remove(ScrollLocker.CSS.scrollLockedHard);\n    if (this.scrollPosition !== null) {\n      window.scrollTo(0, this.scrollPosition);\n    }\n    this.scrollPosition = null;\n  }\n}\n"
  },
  {
    "path": "src/components/utils/shortcuts.ts",
    "content": "import Shortcut from '@codexteam/shortcuts';\n\n/**\n * Contains keyboard and mouse events binded on each Block by Block Manager\n */\n\n/**\n * ShortcutData interface\n * Each shortcut must have name and handler\n * `name` is a shortcut, like 'CMD+K', 'CMD+B' etc\n * `handler` is a callback\n *\n * @interface ShortcutData\n */\nexport interface ShortcutData {\n\n  /**\n   * Shortcut name\n   * Ex. CMD+I, CMD+B ....\n   */\n  name: string;\n\n  /**\n   * Shortcut handler\n   */\n  handler(event): void;\n\n  /**\n   * Element handler should be added for\n   */\n  on: HTMLElement | Document;\n}\n\n/**\n * @class Shortcut\n * @classdesc Allows to register new shortcut\n *\n * Internal Shortcuts Module\n */\nclass Shortcuts {\n  /**\n   * All registered shortcuts\n   *\n   * @type {Map<Element, Shortcut[]>}\n   */\n  private registeredShortcuts: Map<Element, Shortcut[]> = new Map();\n\n  /**\n   * Register shortcut\n   *\n   * @param shortcut - shortcut options\n   */\n  public add(shortcut: ShortcutData): void {\n    const foundShortcut = this.findShortcut(shortcut.on, shortcut.name);\n\n    if (foundShortcut) {\n      throw Error(\n        `Shortcut ${shortcut.name} is already registered for ${shortcut.on}. Please remove it before add a new handler.`\n      );\n    }\n\n    const newShortcut = new Shortcut({\n      name: shortcut.name,\n      on: shortcut.on,\n      callback: shortcut.handler,\n    });\n    const shortcuts = this.registeredShortcuts.get(shortcut.on) || [];\n\n    this.registeredShortcuts.set(shortcut.on, [...shortcuts, newShortcut]);\n  }\n\n  /**\n   * Remove shortcut\n   *\n   * @param element - Element shortcut is set for\n   * @param name - shortcut name\n   */\n  public remove(element: Element, name: string): void {\n    const shortcut = this.findShortcut(element, name);\n\n    if (!shortcut) {\n      return;\n    }\n\n    shortcut.remove();\n\n    const shortcuts = this.registeredShortcuts.get(element);\n\n    const filteredShortcuts = shortcuts.filter(el => el !== shortcut);\n\n    if (filteredShortcuts.length === 0) {\n      this.registeredShortcuts.delete(element);\n\n      return;\n    }\n\n    this.registeredShortcuts.set(element, filteredShortcuts);\n  }\n\n  /**\n   * Get Shortcut instance if exist\n   *\n   * @param element - Element shorcut is set for\n   * @param shortcut - shortcut name\n   * @returns {number} index - shortcut index if exist\n   */\n  private findShortcut(element: Element, shortcut: string): Shortcut | void {\n    const shortcuts = this.registeredShortcuts.get(element) || [];\n\n    return shortcuts.find(({ name }) => name === shortcut);\n  }\n}\n\nexport default new Shortcuts();\n"
  },
  {
    "path": "src/components/utils/tools.ts",
    "content": "import type BlockToolAdapter from '../tools/block';\nimport { isFunction, isString } from '../utils';\n\n/**\n * Check if tool has valid conversion config for export or import.\n *\n * @param tool - tool to check\n * @param direction - export for tool to merge from, import for tool to merge to\n */\nexport function isToolConvertable(tool: BlockToolAdapter, direction: 'export' | 'import'): boolean {\n  if (!tool.conversionConfig) {\n    return false;\n  }\n\n  const conversionProp = tool.conversionConfig[direction];\n\n  return isFunction(conversionProp) || isString(conversionProp);\n}\n"
  },
  {
    "path": "src/components/utils/tooltip.ts",
    "content": "/* eslint-disable jsdoc/no-undefined-types */\n/**\n * Use external module CodeX Tooltip\n */\nimport CodeXTooltips from 'codex-tooltip';\nimport type { TooltipOptions, TooltipContent } from 'codex-tooltip/types';\n\n/**\n * Tooltips lib: CodeX Tooltips\n *\n * @see https://github.com/codex-team/codex.tooltips\n */\nlet lib: null | CodeXTooltips = null;\n\n/**\n * If library is needed, but it is not initialized yet, this function will initialize it\n *\n * For example, if editor was destroyed and then initialized again\n */\nfunction prepare(): void {\n  if (lib) {\n    return;\n  }\n\n  lib = new CodeXTooltips();\n}\n\n/**\n * Shows tooltip on element with passed HTML content\n *\n * @param {HTMLElement} element - any HTML element in DOM\n * @param content - tooltip's content\n * @param options - showing settings\n */\nexport function show(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void {\n  prepare();\n\n  lib?.show(element, content, options);\n}\n\n/**\n * Hides tooltip\n *\n * @param skipHidingDelay — pass true to immediately hide the tooltip\n */\nexport function hide(skipHidingDelay = false): void {\n  prepare();\n\n  lib?.hide(skipHidingDelay);\n}\n\n/**\n * Binds 'mouseenter' and 'mouseleave' events that shows/hides the Tooltip\n *\n * @param {HTMLElement} element - any HTML element in DOM\n * @param content - tooltip's content\n * @param options - showing settings\n */\nexport function onHover(element: HTMLElement, content: TooltipContent, options?: TooltipOptions): void {\n  prepare();\n\n  lib?.onHover(element, content, options);\n}\n\n/**\n * Release the library\n */\nexport function destroy(): void {\n  lib?.destroy();\n  lib = null;\n}\n"
  },
  {
    "path": "src/components/utils.ts",
    "content": "/**\n * Class Util\n */\n\nimport { nanoid } from 'nanoid';\nimport Dom from './dom';\n\n/**\n * Possible log levels\n */\nexport enum LogLevels {\n  VERBOSE = 'VERBOSE',\n  INFO = 'INFO',\n  WARN = 'WARN',\n  ERROR = 'ERROR',\n}\n\n/**\n * Allow to use global VERSION, that will be overwritten by Webpack\n */\ndeclare const VERSION: string;\n\n/**\n * @typedef {object} ChainData\n * @property {object} data - data that will be passed to the success or fallback\n * @property {Function} function - function's that must be called asynchronously\n * @interface ChainData\n */\nexport interface ChainData {\n  data?: object;\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  function: (...args: any[]) => any;\n}\n\n/**\n * Editor.js utils\n */\n\n/**\n * Returns basic key codes as constants\n *\n * @returns {{}}\n */\nexport const keyCodes = {\n  BACKSPACE: 8,\n  TAB: 9,\n  ENTER: 13,\n  SHIFT: 16,\n  CTRL: 17,\n  ALT: 18,\n  ESC: 27,\n  SPACE: 32,\n  LEFT: 37,\n  UP: 38,\n  DOWN: 40,\n  RIGHT: 39,\n  DELETE: 46,\n  META: 91,\n  SLASH: 191,\n};\n\n/**\n * Return mouse buttons codes\n */\nexport const mouseButtons = {\n  LEFT: 0,\n  WHEEL: 1,\n  RIGHT: 2,\n  BACKWARD: 3,\n  FORWARD: 4,\n};\n\n/**\n * Custom logger\n *\n * @param {boolean} labeled — if true, Editor.js label is shown\n * @param {string} msg  - message\n * @param {string} type - logging type 'log'|'warn'|'error'|'info'\n * @param {*} [args]      - argument to log with a message\n * @param {string} style  - additional styling to message\n */\nfunction _log(\n  labeled: boolean,\n  msg: string,\n  type = 'log',\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  args?: any,\n  style = 'color: inherit'\n): void {\n  if (!('console' in window) || !window.console[type]) {\n    return;\n  }\n\n  const isSimpleType = ['info', 'log', 'warn', 'error'].includes(type);\n  const argsToPass = [];\n\n  switch (_log.logLevel) {\n    case LogLevels.ERROR:\n      if (type !== 'error') {\n        return;\n      }\n      break;\n\n    case LogLevels.WARN:\n      if (!['error', 'warn'].includes(type)) {\n        return;\n      }\n      break;\n\n    case LogLevels.INFO:\n      if (!isSimpleType || labeled) {\n        return;\n      }\n      break;\n  }\n\n  if (args) {\n    argsToPass.push(args);\n  }\n\n  const editorLabelText = `Editor.js ${VERSION}`;\n  const editorLabelStyle = `line-height: 1em;\n            color: #006FEA;\n            display: inline-block;\n            font-size: 11px;\n            line-height: 1em;\n            background-color: #fff;\n            padding: 4px 9px;\n            border-radius: 30px;\n            border: 1px solid rgba(56, 138, 229, 0.16);\n            margin: 4px 5px 4px 0;`;\n\n  if (labeled) {\n    if (isSimpleType) {\n      argsToPass.unshift(editorLabelStyle, style);\n      msg = `%c${editorLabelText}%c ${msg}`;\n    } else {\n      msg = `( ${editorLabelText} )${msg}`;\n    }\n  }\n\n  try {\n    if (!isSimpleType) {\n      console[type](msg);\n    } else if (args) {\n      console[type](`${msg} %o`, ...argsToPass);\n    } else {\n      console[type](msg, ...argsToPass);\n    }\n  } catch (ignored) {}\n}\n\n/**\n * Current log level\n */\n_log.logLevel = LogLevels.VERBOSE;\n\n/**\n * Set current log level\n *\n * @param {LogLevels} logLevel - log level to set\n */\nexport function setLogLevel(logLevel: LogLevels): void {\n  _log.logLevel = logLevel;\n}\n\n/**\n * _log method proxy without Editor.js label\n */\nexport const log = _log.bind(window, false);\n\n/**\n * _log method proxy with Editor.js label\n */\nexport const logLabeled = _log.bind(window, true);\n\n/**\n * Return string representation of the object type\n *\n * @param {*} object - object to get type\n * @returns {string}\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function typeOf(object: any): string {\n  return Object.prototype.toString.call(object).match(/\\s([a-zA-Z]+)/)[1].toLowerCase();\n}\n\n/**\n * Check if passed variable is a function\n *\n * @param {*} fn - function to check\n * @returns {boolean}\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function isFunction(fn: any): fn is (...args: any[]) => any {\n  return typeOf(fn) === 'function' || typeOf(fn) === 'asyncfunction';\n}\n\n/**\n * Checks if passed argument is an object\n *\n * @param {*} v - object to check\n * @returns {boolean}\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function isObject(v: any): v is object {\n  return typeOf(v) === 'object';\n}\n\n/**\n * Checks if passed argument is a string\n *\n * @param {*} v - variable to check\n * @returns {boolean}\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function isString(v: any): v is string {\n  return typeOf(v) === 'string';\n}\n\n/**\n * Checks if passed argument is boolean\n *\n * @param {*} v - variable to check\n * @returns {boolean}\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function isBoolean(v: any): v is boolean {\n  return typeOf(v) === 'boolean';\n}\n\n/**\n * Checks if passed argument is number\n *\n * @param {*} v - variable to check\n * @returns {boolean}\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function isNumber(v: any): v is number {\n  return typeOf(v) === 'number';\n}\n\n/**\n * Checks if passed argument is undefined\n *\n * @param {*} v - variable to check\n * @returns {boolean}\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function isUndefined(v: any): v is undefined {\n  return typeOf(v) === 'undefined';\n}\n\n/**\n * Check if passed function is a class\n *\n * @param {Function} fn - function to check\n * @returns {boolean}\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function isClass(fn: any): boolean {\n  return isFunction(fn) && /^\\s*class\\s+/.test(fn.toString());\n}\n\n/**\n * Checks if object is empty\n *\n * @param {object} object - object to check\n * @returns {boolean}\n */\nexport function isEmpty(object: object): boolean {\n  if (!object) {\n    return true;\n  }\n\n  return Object.keys(object).length === 0 && object.constructor === Object;\n}\n\n/**\n * Check if passed object is a Promise\n *\n * @param  {*}  object - object to check\n * @returns {boolean}\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function isPromise(object: any): object is Promise<any> {\n  return Promise.resolve(object) === object;\n}\n\n/* eslint-disable @typescript-eslint/no-magic-numbers */\n/**\n * Returns true if passed key code is printable (a-Z, 0-9, etc) character.\n *\n * @param {number} keyCode - key code\n * @returns {boolean}\n */\nexport function isPrintableKey(keyCode: number): boolean {\n  return (keyCode > 47 && keyCode < 58) || // number keys\n    keyCode === 32 || keyCode === 13 || // Space bar & return key(s)\n    keyCode === 229 || // processing key input for certain languages — Chinese, Japanese, etc.\n    (keyCode > 64 && keyCode < 91) || // letter keys\n    (keyCode > 95 && keyCode < 112) || // Numpad keys\n    (keyCode > 185 && keyCode < 193) || // ;=,-./` (in order)\n    (keyCode > 218 && keyCode < 223); // [\\]' (in order)\n}\n/* eslint-enable @typescript-eslint/no-magic-numbers */\n\n/**\n * Fires a promise sequence asynchronously\n *\n * @param {ChainData[]} chains - list or ChainData's\n * @param {Function} success - success callback\n * @param {Function} fallback - callback that fires in case of errors\n * @returns {Promise}\n * @deprecated use PromiseQueue.ts instead\n */\nexport async function sequence(\n  chains: ChainData[],\n  // eslint-disable-next-line @typescript-eslint/no-empty-function\n  success: (data: object) => void = (): void => {},\n  // eslint-disable-next-line @typescript-eslint/no-empty-function\n  fallback: (data: object) => void = (): void => {}\n): Promise<void> {\n  /**\n   * Decorator\n   *\n   * @param {ChainData} chainData - Chain data\n   * @param {Function} successCallback - success callback\n   * @param {Function} fallbackCallback - fail callback\n   * @returns {Promise}\n   */\n  async function waitNextBlock(\n    chainData: ChainData,\n    successCallback: (data: object) => void,\n    fallbackCallback: (data: object) => void\n  ): Promise<void> {\n    try {\n      await chainData.function(chainData.data);\n      await successCallback(!isUndefined(chainData.data) ? chainData.data : {});\n    } catch (e) {\n      fallbackCallback(!isUndefined(chainData.data) ? chainData.data : {});\n    }\n  }\n\n  /**\n   * pluck each element from queue\n   * First, send resolved Promise as previous value\n   * Each plugins \"prepare\" method returns a Promise, that's why\n   * reduce current element will not be able to continue while can't get\n   * a resolved Promise\n   */\n  return chains.reduce(async (previousValue, currentValue) => {\n    await previousValue;\n\n    return waitNextBlock(currentValue, success, fallback);\n  }, Promise.resolve());\n}\n\n/**\n * Make array from array-like collection\n *\n * @param {ArrayLike} collection - collection to convert to array\n * @returns {Array}\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function array(collection: ArrayLike<any>): any[] {\n  return Array.prototype.slice.call(collection);\n}\n\n/**\n * Delays method execution\n *\n * @param {Function} method - method to execute\n * @param {number} timeout - timeout in ms\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function delay(method: (...args: any[]) => any, timeout: number) {\n  return function (): void {\n    // eslint-disable-next-line @typescript-eslint/no-this-alias\n    const context = this,\n        // eslint-disable-next-line prefer-rest-params\n        args = arguments;\n\n    window.setTimeout(() => method.apply(context, args), timeout);\n  };\n}\n\n/**\n * Get file extension\n *\n * @param {File} file - file\n * @returns {string}\n */\nexport function getFileExtension(file: File): string {\n  return file.name.split('.').pop();\n}\n\n/**\n * Check if string is MIME type\n *\n * @param {string} type - string to check\n * @returns {boolean}\n */\nexport function isValidMimeType(type: string): boolean {\n  return /^[-\\w]+\\/([-+\\w]+|\\*)$/.test(type);\n}\n\n/**\n * Debouncing method\n * Call method after passed time\n *\n * Note that this method returns Function and declared variable need to be called\n *\n * @param {Function} func - function that we're throttling\n * @param {number} wait - time in milliseconds\n * @param {boolean} immediate - call now\n * @returns {Function}\n */\nexport function debounce(func: (...args: unknown[]) => void, wait?: number, immediate?: boolean): () => void {\n  let timeout;\n\n  return (...args: unknown[]): void => {\n    // eslint-disable-next-line @typescript-eslint/no-this-alias\n    const context = this;\n\n    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type\n    const later = () => {\n      timeout = null;\n      if (!immediate) {\n        func.apply(context, args);\n      }\n    };\n\n    const callNow = immediate && !timeout;\n\n    window.clearTimeout(timeout);\n    timeout = window.setTimeout(later, wait);\n    if (callNow) {\n      func.apply(context, args);\n    }\n  };\n}\n\n/**\n * Returns a function, that, when invoked, will only be triggered at most once during a given window of time.\n *\n * @param func - function to throttle\n * @param wait - function will be called only once for that period\n * @param options - Normally, the throttled function will run as much as it can\n *                  without ever going more than once per `wait` duration;\n *                  but if you'd like to disable the execution on the leading edge, pass\n *                  `{leading: false}`. To disable execution on the trailing edge, ditto.\n */\nexport function throttle(func, wait, options: {leading?: boolean; trailing?: boolean} = undefined): () => void {\n  let context, args, result;\n  let timeout = null;\n  let previous = 0;\n\n  if (!options) {\n    options = {};\n  }\n\n  const later = function (): void {\n    previous = options.leading === false ? 0 : Date.now();\n    timeout = null;\n    result = func.apply(context, args);\n\n    if (!timeout) {\n      context = args = null;\n    }\n  };\n\n  return function (): unknown {\n    const now = Date.now();\n\n    if (!previous && options.leading === false) {\n      previous = now;\n    }\n\n    const remaining = wait - (now - previous);\n\n    // eslint-disable-next-line @typescript-eslint/no-this-alias\n    context = this;\n\n    // eslint-disable-next-line prefer-rest-params\n    args = arguments;\n\n    if (remaining <= 0 || remaining > wait) {\n      if (timeout) {\n        clearTimeout(timeout);\n        timeout = null;\n      }\n      previous = now;\n      result = func.apply(context, args);\n\n      if (!timeout) {\n        context = args = null;\n      }\n    } else if (!timeout && options.trailing !== false) {\n      timeout = setTimeout(later, remaining);\n    }\n\n    return result;\n  };\n}\n\n/**\n * Copies passed text to the clipboard\n *\n * @param text - text to copy\n */\nexport function copyTextToClipboard(text): void {\n  const el = Dom.make('div', 'codex-editor-clipboard', {\n    innerHTML: text,\n  });\n\n  document.body.appendChild(el);\n\n  const selection = window.getSelection();\n  const range = document.createRange();\n\n  range.selectNode(el);\n\n  window.getSelection().removeAllRanges();\n  selection.addRange(range);\n\n  document.execCommand('copy');\n  document.body.removeChild(el);\n}\n\n/**\n * Returns object with os name as key and boolean as value. Shows current user OS\n */\nexport function getUserOS(): {[key: string]: boolean} {\n  const OS = {\n    win: false,\n    mac: false,\n    x11: false,\n    linux: false,\n  };\n\n  const userOS = Object.keys(OS).find((os: string) => window.navigator.appVersion.toLowerCase().indexOf(os) !== -1);\n\n  if (userOS) {\n    OS[userOS] = true;\n\n    return OS;\n  }\n\n  return OS;\n}\n\n/**\n * Capitalizes first letter of the string\n *\n * @param {string} text - text to capitalize\n * @returns {string}\n */\nexport function capitalize(text: string): string {\n  return text[0].toUpperCase() + text.slice(1);\n}\n\n/**\n * Merge to objects recursively\n *\n * @param {object} target - merge target\n * @param {object[]} sources - merge sources\n * @returns {object}\n */\nexport function deepMerge<T extends object>(target, ...sources): T {\n  if (!sources.length) {\n    return target;\n  }\n  const source = sources.shift();\n\n  if (isObject(target) && isObject(source)) {\n    for (const key in source) {\n      if (isObject(source[key])) {\n        if (!target[key]) {\n          Object.assign(target, { [key]: {} });\n        }\n\n        deepMerge(target[key], source[key]);\n      } else {\n        Object.assign(target, { [key]: source[key] });\n      }\n    }\n  }\n\n  return deepMerge(target, ...sources);\n}\n\n/**\n * Return true if current device supports touch events\n *\n * Note! This is a simple solution, it can give false-positive results.\n * To detect touch devices more carefully, use 'touchstart' event listener\n *\n * @see http://www.stucox.com/blog/you-cant-detect-a-touchscreen/\n * @returns {boolean}\n */\nexport const isTouchSupported: boolean = 'ontouchstart' in document.documentElement;\n\n/**\n * Make shortcut command more human-readable\n *\n * @param {string} shortcut — string like 'CMD+B'\n */\nexport function beautifyShortcut(shortcut: string): string {\n  const OS = getUserOS();\n\n  shortcut = shortcut\n    .replace(/shift/gi, '⇧')\n    .replace(/backspace/gi, '⌫')\n    .replace(/enter/gi, '⏎')\n    .replace(/up/gi, '↑')\n    .replace(/left/gi, '→')\n    .replace(/down/gi, '↓')\n    .replace(/right/gi, '←')\n    .replace(/escape/gi, '⎋')\n    .replace(/insert/gi, 'Ins')\n    .replace(/delete/gi, '␡')\n    .replace(/\\+/gi, ' + ');\n\n  if (OS.mac) {\n    shortcut = shortcut.replace(/ctrl|cmd/gi, '⌘').replace(/alt/gi, '⌥');\n  } else {\n    shortcut = shortcut.replace(/cmd/gi, 'Ctrl').replace(/windows/gi, 'WIN');\n  }\n\n  return shortcut;\n}\n\n/**\n * Returns valid URL. If it is going outside and valid, it returns itself\n * If url has `one slash`, then it concatenates with window location origin\n * or when url has `two lack` it appends only protocol\n *\n * @param {string} url - url to prettify\n */\nexport function getValidUrl(url: string): string {\n  try {\n    const urlObject = new URL(url);\n\n    return urlObject.href;\n  } catch (e) {\n    // do nothing but handle below\n  }\n\n  if (url.substring(0, 2) === '//') {\n    return window.location.protocol + url;\n  } else {\n    return window.location.origin + url;\n  }\n}\n\n/**\n * Create a block id\n *\n * @returns {string}\n */\nexport function generateBlockId(): string {\n  const idLen = 10;\n\n  return nanoid(idLen);\n}\n\n/**\n * Opens new Tab with passed URL\n *\n * @param {string} url - URL address to redirect\n */\nexport function openTab(url: string): void {\n  window.open(url, '_blank');\n}\n\n/**\n * Returns random generated identifier\n *\n * @param {string} prefix - identifier prefix\n * @returns {string}\n */\nexport function generateId(prefix = ''): string {\n  // eslint-disable-next-line @typescript-eslint/no-magic-numbers\n  return `${prefix}${(Math.floor(Math.random() * 1e8)).toString(16)}`;\n}\n\n/**\n * Common method for printing a warning about the usage of deprecated property or method.\n *\n * @param condition - condition for deprecation.\n * @param oldProperty - deprecated property.\n * @param newProperty - the property that should be used instead.\n */\nexport function deprecationAssert(condition: boolean, oldProperty: string, newProperty: string): void {\n  const message = `«${oldProperty}» is deprecated and will be removed in the next major release. Please use the «${newProperty}» instead.`;\n\n  if (condition) {\n    logLabeled(message, 'warn');\n  }\n}\n\n/**\n * Decorator which provides ability to cache method or accessor result\n *\n * @param target - target instance or constructor function\n * @param propertyKey - method or accessor name\n * @param descriptor - property descriptor\n */\nexport function cacheable<Target, Value, Arguments extends unknown[] = unknown[]>(\n  target: Target,\n  propertyKey: string,\n  descriptor: PropertyDescriptor\n): PropertyDescriptor {\n  const propertyToOverride = descriptor.value ? 'value' : 'get';\n  const originalMethod = descriptor[propertyToOverride];\n  const cacheKey = `#${propertyKey}Cache`;\n\n  /**\n   * Override get or value descriptor property to cache return value\n   *\n   * @param args - method args\n   */\n  descriptor[propertyToOverride] = function (...args: Arguments): Value {\n    /**\n     * If there is no cache, create it\n     */\n    if (this[cacheKey] === undefined) {\n      this[cacheKey] = originalMethod.apply(this, ...args);\n    }\n\n    return this[cacheKey];\n  };\n\n  /**\n   * If get accessor has been overridden, we need to override set accessor to clear cache\n   *\n   * @param value - value to set\n   */\n  if (propertyToOverride === 'get' && descriptor.set) {\n    const originalSet = descriptor.set;\n\n    descriptor.set = function (value: unknown): void {\n      delete target[cacheKey];\n\n      originalSet.apply(this, value);\n    };\n  }\n\n  return descriptor;\n}\n\n/**\n * All screens below this width will be treated as mobile;\n */\nexport const mobileScreenBreakpoint = 650;\n\n/**\n * True if screen has mobile size\n */\nexport function isMobileScreen(): boolean {\n  return window.matchMedia(`(max-width: ${mobileScreenBreakpoint}px)`).matches;\n}\n\n/**\n * True if current device runs iOS\n */\nexport const isIosDevice =\n  typeof window !== 'undefined' &&\n  window.navigator &&\n  window.navigator.platform &&\n  (/iP(ad|hone|od)/.test(window.navigator.platform) ||\n    (window.navigator.platform === 'MacIntel' && window.navigator.maxTouchPoints > 1));\n\n/**\n * Compares two values with unknown type\n *\n * @param var1 - value to compare\n * @param var2 - value to compare with\n * @returns {boolean} true if they are equal\n */\nexport function equals(var1: unknown, var2: unknown): boolean {\n  const isVar1NonPrimitive = Array.isArray(var1) || isObject(var1);\n  const isVar2NonPrimitive = Array.isArray(var2) || isObject(var2);\n\n  if (isVar1NonPrimitive || isVar2NonPrimitive) {\n    return JSON.stringify(var1) === JSON.stringify(var2);\n  }\n\n  return var1 === var2;\n}\n"
  },
  {
    "path": "src/env.d.ts",
    "content": "interface ImportMetaEnv {\n  /**\n   * Build environment.\n   * For example, used to detect building for tests and add \"data-cy\" attributes for DOM querying.\n   */\n  readonly MODE: \"test\" | \"development\" | \"production\";\n}\n\ninterface ImportMeta {\n  readonly env: ImportMetaEnv;\n}\n"
  },
  {
    "path": "src/styles/animations.css",
    "content": "@keyframes bounceIn {\n  from,\n  20%,\n  40%,\n  60%,\n  80%,\n  to {\n    animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);\n  }\n\n  0% {\n    transform: scale3d(0.9, 0.9, 0.9);\n  }\n\n  20% {\n    transform: scale3d(1.03, 1.03, 1.03);\n  }\n\n  60% {\n    transform: scale3d(1, 1, 1);\n  }\n}\n\n@keyframes selectionBounce {\n  from,\n  20%,\n  40%,\n  60%,\n  80%,\n  to {\n    animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);\n  }\n\n  50% {\n    transform: scale3d(1.01, 1.01, 1.01);\n  }\n\n  70% {\n    transform: scale3d(1, 1, 1);\n  }\n}\n\n@keyframes buttonClicked {\n  from,\n  20%,\n  40%,\n  60%,\n  80%,\n  to {\n    animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);\n  }\n\n  0% {\n    transform: scale3d(0.95, 0.95, 0.95);\n  }\n\n  60% {\n    transform: scale3d(1.02, 1.02, 1.02);\n  }\n\n  80% {\n    transform: scale3d(1, 1, 1);\n  }\n}\n\n"
  },
  {
    "path": "src/styles/block.css",
    "content": "@keyframes fade-in {\n  from {\n    opacity: 0;\n  }\n\n  to {\n    opacity: 1;\n  }\n}\n\n.ce-block {\n  animation: fade-in 300ms ease;\n  animation-fill-mode: initial;\n\n  &:first-of-type {\n    margin-top: 0;\n  }\n\n  &--selected &__content {\n    background: var(--selectionColor);\n\n    /**\n     * Workaround Safari case when user can select inline-fragment with cross-block-selection\n     */\n    & [contenteditable] {\n      -webkit-user-select: none;\n      user-select: none;\n    }\n\n    img,\n    .ce-stub {\n      opacity: 0.55;\n    }\n  }\n\n  &--stretched &__content {\n    max-width: none;\n  }\n\n  &__content {\n    position: relative;\n    max-width: var(--content-width);\n    margin: 0 auto;\n    transition: background-color 150ms ease;\n  }\n\n  &--drop-target &__content {\n    &:before {\n      content: '';\n      position: absolute;\n      top: 100%;\n      left: -20px;\n      margin-top: -1px;\n      height: 8px;\n      width: 8px;\n      border: solid var(--color-active-icon);\n      border-width: 1px 1px 0 0;\n      transform-origin: right;\n      transform: rotate(45deg);\n    }\n\n    &:after {\n      content: '';\n      position: absolute;\n      top: 100%;\n      height: 1px;\n      width: 100%;\n      color: var(--color-active-icon);\n      background: repeating-linear-gradient(\n        90deg,\n        var(--color-active-icon),\n        var(--color-active-icon) 1px,\n        #fff 1px,\n        #fff 6px\n      );\n    }\n  }\n\n  a {\n    cursor: pointer;\n    text-decoration: underline;\n  }\n\n  b {\n    font-weight: bold;\n  }\n\n  i {\n    font-style: italic;\n  }\n}\n"
  },
  {
    "path": "src/styles/export.css",
    "content": "/**\n * Block Tool wrapper\n */\n.cdx-block {\n  padding: var(--block-padding-vertical) 0;\n\n  &::-webkit-input-placeholder {\n    line-height:normal!important;\n  }\n}\n\n/**\n * Input\n */\n.cdx-input {\n  border: 1px solid var(--color-gray-border);\n  box-shadow: inset 0 1px 2px 0 rgba(35, 44, 72, 0.06);\n  border-radius: 3px;\n  padding: 10px 12px;\n  outline: none;\n  width: 100%;\n  box-sizing: border-box;\n\n  /**\n   * Workaround Firefox bug with cursor position on empty content editable elements with ::before pseudo\n   * https://bugzilla.mozilla.org/show_bug.cgi?id=904846\n   */\n  &[data-placeholder]::before {\n    position: static !important;\n    display: inline-block;\n    width: 0;\n    white-space: nowrap;\n    pointer-events: none;\n  }\n}\n\n/**\n * Settings\n * @deprecated - use tunes config instead of creating html element with controls\n */\n.cdx-settings-button {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n\n  border-radius: 3px;\n  cursor: pointer;\n  border: 0;\n  outline: none;\n  background-color: transparent;\n  vertical-align: bottom;\n  color: inherit;\n  margin: 0;\n  min-width: var(--toolbox-buttons-size);\n  min-height: var(--toolbox-buttons-size);\n\n  &--focused {\n    @apply --button-focused;\n\n    &-animated {\n      animation-name: buttonClicked;\n      animation-duration: 250ms;\n    }\n  }\n\n  &--active {\n    color: var(--color-active-icon);\n  }\n\n  svg {\n    width: auto;\n    height: auto;\n\n    @media (--mobile) {\n      width: var(--icon-size--mobile);\n      height: var(--icon-size--mobile);\n    }\n  }\n\n  @media (--mobile) {\n    width: var(--toolbox-buttons-size--mobile);\n    height: var(--toolbox-buttons-size--mobile);\n    border-radius: 8px;\n  }\n\n  @media (--can-hover) {\n    &:hover {\n      background-color: var(--bg-light);\n    }\n  }\n}\n\n/**\n * Loader\n */\n.cdx-loader {\n  position: relative;\n  border: 1px solid var(--color-gray-border);\n\n  &::before {\n    content: '';\n    position: absolute;\n    left: 50%;\n    top: 50%;\n    width: 18px;\n    height: 18px;\n    margin: -11px 0 0 -11px;\n    border: 2px solid var(--color-gray-border);\n    border-left-color: var(--color-active-icon);\n    border-radius: 50%;\n    animation: cdxRotation 1.2s infinite linear;\n  }\n}\n\n@keyframes cdxRotation {\n  0% {\n    transform: rotate(0deg);\n  }\n  100% {\n    transform: rotate(360deg);\n  }\n}\n\n/**\n * Button\n */\n.cdx-button {\n  padding: 13px;\n  border-radius: 3px;\n  border: 1px solid var(--color-gray-border);\n  font-size: 14.9px;\n  background: #fff;\n  box-shadow: 0 2px 2px 0 rgba(18,30,57,0.04);\n  color: var(--grayText);\n  text-align: center;\n  cursor: pointer;\n\n  @media (--can-hover) {\n    &:hover {\n      background: #FBFCFE;\n      box-shadow: 0 1px 3px 0 rgba(18,30,57,0.08);\n    }\n  }\n\n  svg {\n    height: 20px;\n    margin-right: 0.2em;\n    margin-top: -2px;\n  }\n}\n"
  },
  {
    "path": "src/styles/inline-toolbar.css",
    "content": ".ce-inline-toolbar {\n  --y-offset: 8px;\n\n  /** These variables duplicate the ones defined in popover. @todo move them to single place */\n  --color-background-icon-active: rgba(56, 138, 229, 0.1);\n  --color-text-icon-active: #388AE5;\n  --color-text-primary: black;\n\n  position: absolute;\n  visibility: hidden;\n  transition: opacity 250ms ease;\n  will-change: opacity, left, top;\n  top: 0;\n  left: 0;\n  z-index: 3;\n  opacity: 1;\n  visibility: visible;\n\n  [hidden] {\n    display: none !important;\n  }\n\n  &__toggler-and-button-wrapper {\n    display: flex;\n    width: 100%;\n    padding: 0 6px;\n  }\n\n  &__buttons {\n    display: flex;\n  }\n\n  &__actions {\n  }\n\n  &__dropdown {\n    display: flex;\n    padding: 6px;\n    margin: 0 6px 0 -6px;\n    align-items: center;\n    cursor: pointer;\n    border-right: 1px solid var(--color-gray-border);\n    box-sizing: border-box;\n\n    @media (--can-hover) {\n      &:hover {\n        background: var(--bg-light);\n      }\n    }\n\n    &--hidden {\n      display: none;\n    }\n\n    &-content,\n    &-arrow {\n      display: flex;\n      svg {\n        width: var(--icon-size);\n        height: var(--icon-size);\n      }\n    }\n  }\n\n  &__shortcut {\n    opacity: 0.6;\n    word-spacing: -3px;\n    margin-top: 3px;\n  }\n}\n\n.ce-inline-tool {\n  color: var(--color-text-primary);\n  display: flex;\n  justify-content: center;\n  align-items: center;\n\n  border: 0;\n  border-radius: 4px;\n  line-height: normal;\n  height: 100%;\n  padding: 0;\n  width: 28px;\n  background-color: transparent;\n  cursor: pointer;\n\n  @media (--mobile) {\n    width: 36px;\n    height: 36px;\n  }\n\n  @media (--can-hover) {\n    &:hover {\n      background-color: #F8F8F8; /* @todo replace with 'var(--color-background-item-hover)' */\n    }\n  }\n\n  svg {\n    display: block;\n    width: var(--icon-size);\n    height: var(--icon-size);\n\n    @media (--mobile) {\n      width: var(--icon-size--mobile);\n      height: var(--icon-size--mobile);\n    }\n  }\n\n  &--link {\n    .icon--unlink {\n      display: none;\n    }\n  }\n\n  &--unlink {\n    .icon--link {\n      display: none;\n    }\n    .icon--unlink {\n      display: inline-block;\n      margin-bottom: -1px;\n    }\n  }\n\n  &-input {\n    background: #F8F8F8;\n    border: 1px solid rgba(226,226,229,0.20);\n    border-radius: 6px;\n    padding: 4px 8px;\n    font-size: 14px;\n    line-height: 22px;\n\n\n    outline: none;\n    margin: 0;\n    width: 100%;\n    box-sizing: border-box;\n    display: none;\n    font-weight: 500;\n    -webkit-appearance: none;\n    font-family: inherit;\n\n    @media (--mobile){\n      font-size: 15px;\n      font-weight: 500;\n    }\n\n    &::placeholder {\n      color: var(--grayText);\n    }\n\n    &--showed {\n      display: block;\n    }\n  }\n\n  &--active {\n    background: var(--color-background-icon-active);\n    color: var(--color-text-icon-active);\n  }\n}\n"
  },
  {
    "path": "src/styles/input.css",
    "content": ".cdx-search-field {\n  --icon-margin-right: 10px;\n\n  background: #F8F8F8;\n  border: 1px solid rgba(226,226,229,0.20);\n  border-radius: 6px;\n  padding: 2px;\n  display: grid;\n  grid-template-columns: auto auto 1fr;\n  grid-template-rows: auto;\n\n  &__icon {\n    width: var(--toolbox-buttons-size);\n    height: var(--toolbox-buttons-size);\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    margin-right: var(--icon-margin-right);\n\n    svg {\n      width: var(--icon-size);\n      height: var(--icon-size);\n      color: var(--grayText);\n    }\n  }\n\n  &__input {\n    font-size: 14px;\n    outline: none;\n    font-weight: 500;\n    font-family: inherit;\n    border: 0;\n    background: transparent;\n    margin: 0;\n    padding: 0;\n    line-height: 22px;\n    min-width: calc(100% - var(--toolbox-buttons-size) - var(--icon-margin-right));\n\n    &::placeholder {\n       color: var(--grayText);\n       font-weight: 500;\n    }\n  }\n}\n"
  },
  {
    "path": "src/styles/main.css",
    "content": "@import './variables.css';\n@import './ui.css';\n@import './toolbar.css';\n@import './toolbox.css';\n@import './inline-toolbar.css';\n@import './block.css';\n@import './animations.css';\n@import './export.css';\n@import './stub.css';\n@import './rtl.css';\n@import './input.css';\n@import './popover.css';\n@import './popover-inline.css';\n@import './placeholders.css';\n\n"
  },
  {
    "path": "src/styles/placeholders.css",
    "content": "\n/**\n * We support two types of placeholders for contenteditable:\n *\n * 1. Regular-like placeholders. Will be visible when element is empty.\n      -- Best choice for rare-used blocks like Headings.\n * 2. Current-block placeholders. Will be visible when element is empty and the block is focused.\n      -- Best choice for common-used blocks like Paragraphs.\n */\n :root {\n  --placeholder {\n    pointer-events: none;\n    color: var(--grayText);\n    cursor: text;\n  }\n }\n\n.codex-editor {\n  /**\n   * Use [data-placeholder=\"...\"] to always show a placeholder on empty contenteditable.\n   */\n  [data-placeholder]:empty,\n  [data-placeholder][data-empty=\"true\"] {\n    &::before {\n      @apply --placeholder;\n\n      content: attr(data-placeholder);\n    }\n  }\n\n   /**\n    * Use [data-placeholder-active=\"...\"] to show a placeholder on empty contenteditable in current block.\n    */\n  [data-placeholder-active]:empty,\n  [data-placeholder-active][data-empty=\"true\"] {\n    /* Paragraph tool shows the placeholder for the first block, event it is not focused, so we need to prepare styles for it */\n    &::before {\n      @apply --placeholder;\n    }\n\n    &:focus::before {\n      content: attr(data-placeholder-active);\n    }\n  }\n}\n"
  },
  {
    "path": "src/styles/popover-inline.css",
    "content": "/**\n * Styles overrides for inline popover\n */\n.ce-popover--inline {\n  --height: 38px;\n  --height-mobile: 46px;\n  --container-padding: 4px;\n\n  position: relative;\n\n  .ce-popover__custom-content {\n    margin-bottom: 0;\n  }\n\n  .ce-popover__items {\n    display: flex;\n  }\n  \n  .ce-popover__container {\n    flex-direction: row;\n    padding: var(--container-padding);\n    height: var(--height);\n    top: 0;\n    \n    min-width: max-content;\n    width: max-content;\n    animation: none;\n\n    @media (--mobile) {\n      height: var(--height-mobile);\n      position: absolute;\n    }\n  }\n\n  /** \n   * Popover item styles\n   */\n  .ce-popover-item-separator {\n    padding: 0 4px;\n\n    &__line {\n      height: 100%;\n      width: 1px;\n    }\n  }\n\n  .ce-popover-item {\n    border-radius: 4px;\n    padding: 4px;\n\n    &__icon--tool {\n      box-shadow: none;\n      background: transparent;\n      margin-right: 0;\n    }\n    \n    &__icon {\n      width: unset;\n      height: unset;\n\n      svg {\n        width: var(--icon-size);\n        height: var(--icon-size);\n\n        @media (--mobile) {\n          width: var(--icon-size--mobile);\n          height: var(--icon-size--mobile);\n        }\n      }\n    }\n\n    &:not(:last-of-type) {\n      margin-bottom: unset;\n    }\n  }\n\n  .ce-popover-item-html {\n    display: flex;\n    align-items: center;\n  }\n\n  .ce-popover-item__icon--chevron-right {\n    transform: rotate(90deg);\n  }\n\n  .ce-popover--nested-level-1 {\n    .ce-popover__container {\n      --offset: 3px;\n\n      left: 0px;\n      top: calc(var(--height) + var(--offset));\n\n      @media (--mobile) {\n        top: calc(var(--height-mobile) + var(--offset));\n      }\n    }\n  }\n\n  /**\n   * Nested popovers should look like regular desktop popovers, hence these overrides\n   */\n  .ce-popover--nested {\n    .ce-popover__container {\n      min-width: var(--width);\n      width: var(--width);\n      height: fit-content;\n      padding: 6px;\n      flex-direction: column;\n    }\n\n    .ce-popover__items {\n      display: block;\n      width: 100%;\n    }\n\n    .ce-popover-item {\n      border-radius: 6px;\n      padding: 3px;\n\n      @media (--mobile) {\n        padding: 4px;\n      }\n    \n      &__icon--tool {\n        margin-right: 4px;\n      }\n\n      &__icon {\n        width: 26px;\n        height: 26px;\n      }\n    }\n\n    .ce-popover-item-separator {\n      padding: 4px 3px;\n\n      &__line {\n        width: 100%;\n        height: 1px;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/styles/popover.css",
    "content": "/** \n * Popover styles\n *\n * @todo split into separate files popover styles\n * @todo make css variables work\n */\n.ce-popover {\n  --border-radius: 6px;\n  --width: 200px;\n  --max-height: 270px;\n  --padding: 6px;\n  --offset-from-target: 8px;\n  --color-border: #EFF0F1;\n  --color-shadow: rgba(13, 20, 33, 0.10);\n  --color-background: white;\n  --color-text-primary: black;\n  --color-text-secondary: #707684;\n  --color-border-icon: rgb(201 201 204 / 48%);\n  --color-border-icon-disabled: #EFF0F1;\n  --color-text-icon-active: #388AE5;\n  --color-background-icon-active: rgba(56, 138, 229, 0.1);\n  --color-background-item-focus: rgba(34, 186, 255, 0.08);\n  --color-shadow-item-focus: rgba(7, 161, 227, 0.08);\n  --color-background-item-hover: #F8F8F8;\n  --color-background-item-confirm: #E24A4A;\n  --color-background-item-confirm-hover: #CE4343;\n  --popover-top: calc(100% + var(--offset-from-target));\n  --popover-left: 0;\n  --nested-popover-overlap: 4px;\n\n  --icon-size: 20px;\n  --item-padding: 3px;\n  --item-height: calc(var(--icon-size) + 2 * var(--item-padding));\n\n  &__container {\n    min-width: var(--width);\n    width: var(--width);\n    max-height: var(--max-height);\n    border-radius: var(--border-radius);\n    overflow: hidden;\n    box-sizing: border-box;\n    box-shadow: 0px 3px 15px -3px var(--color-shadow);\n    position: absolute;\n    left: var(--popover-left);\n    top: var(--popover-top);\n\n    background: var(--color-background);\n    display: flex;\n    flex-direction: column;\n    z-index: 4;\n  \n    opacity: 0;\n    max-height: 0;\n    pointer-events: none;\n    padding: 0;\n    border: none;\n  }\n\n  &--opened {\n    & > .ce-popover__container {\n      opacity: 1;\n      padding: var(--padding);\n      max-height: var(--max-height);\n      pointer-events: auto;\n      animation: panelShowing 100ms ease;\n      border: 1px solid var(--color-border);\n  \n      @media (--mobile) {\n        animation: panelShowingMobile 250ms ease;\n      }\n    }\n  }\n\n  &--open-top {\n    .ce-popover__container {\n      --popover-top: calc(-1 * (var(--offset-from-target) + var(--popover-height)));\n    }\n  }\n\n  &--open-left {\n    .ce-popover__container {\n      --popover-left: calc(-1 * var(--width) + 100%);\n    }\n  }\n\n  &__items {\n    overflow-y: auto;\n    overscroll-behavior: contain;\n  }\n\n  &__overlay {\n    @media (--mobile) {\n      position: fixed;\n      top: 0;\n      bottom: 0;\n      left: 0;\n      right: 0;\n      background: var(--color-dark);\n      z-index: 3;\n      opacity: 0.5;\n      transition: opacity 0.12s ease-in;\n      will-change: opacity;\n      visibility: visible;\n    }\n\n    &--hidden {\n      display: none;\n    }\n  }\n\n\n  @media (--mobile) {\n\n    .ce-popover__container {\n      --offset: 5px;\n  \n      position: fixed;\n      max-width: none;\n      min-width: calc(100% - var(--offset) * 2);\n      left: var(--offset);\n      right: var(--offset);\n      bottom: calc(var(--offset) + env(safe-area-inset-bottom));\n      top: auto;\n      border-radius: 10px;\n    }\n  }\n\n  &__search {\n    margin-bottom: 5px;\n  }\n\n  &__nothing-found-message {\n    color: var(--grayText);\n    display: none;\n    cursor: default;\n    padding: 3px;\n    font-size: 14px;\n    line-height: 20px;\n    font-weight: 500;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n\n    &--displayed {\n      display: block;\n    }\n  }\n\n  &--nested {\n    .ce-popover__container {\n      /* Variable --nesting-level is set via js in showNestedPopoverForItem() method */\n      --popover-left: calc(var(--nesting-level) * (var(--width) - var(--nested-popover-overlap)));\n      /* Variable --trigger-item-top is set via js in showNestedPopoverForItem() method */\n      top: calc(var(--trigger-item-top) - var(--nested-popover-overlap));\n      position: absolute;\n    }\n  }\n\n  &--open-top.ce-popover--nested {\n    .ce-popover__container {\n      /** Bottom edge of nested popover should not be lower than bottom edge of parent popover when opened upwards */\n      top: calc(var(--trigger-item-top) - var(--popover-height) + var(--item-height) + var(--offset-from-target) + var(--nested-popover-overlap));\n\n    }\n  }\n\n  &--open-left {\n    .ce-popover--nested {\n      .ce-popover__container {\n        --popover-left: calc(-1 * (var(--nesting-level) + 1) * var(--width) + 100%);\n      }\n    }\n  }\n}\n\n\n/** \n * Popover item styles\n */\n.ce-popover-item-separator {\n  padding: 4px 3px;\n\n  &--hidden {\n    display: none;\n  }\n\n  &__line {\n    height: 1px;\n    background: var(--color-border);\n    width: 100%;\n  }\n}\n\n.ce-popover-item-html {\n  &--hidden {\n    display: none;\n  }\n}\n\n.ce-popover-item {\n  --border-radius: 6px;\n  border-radius: var(--border-radius);\n  display: flex;\n  align-items: center;\n  padding: var(--item-padding);\n  color: var(--color-text-primary);\n  user-select: none;\n  border: none;\n  background: transparent;\n\n  @media (--mobile) {\n    padding: 4px;\n  }\n\n  &:not(:last-of-type) {\n    margin-bottom: 1px;\n  }\n\n  &__icon {\n    width: 26px;\n    height: 26px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n\n    svg {\n      width: var(--icon-size);\n      height: var(--icon-size);\n    }\n\n    @media (--mobile){\n      width: 36px;\n      height: 36px;\n      border-radius: 8px;\n\n      svg {\n        width: 28px;\n        height: 28px;\n      }\n    }\n  }\n\n  &__icon--tool {\n    margin-right: 4px;\n  }\n\n  &__title {\n    font-size: 14px;\n    line-height: 20px;\n    font-weight: 500;\n\n    overflow: hidden;\n    white-space: nowrap;\n    text-overflow: ellipsis;\n\n    margin-right: auto;\n\n    @media (--mobile) {\n      font-size: 16px;\n    }\n  }\n\n  &__secondary-title {\n    color: var(--color-text-secondary);\n    font-size: 12px;\n    white-space: nowrap;\n    letter-spacing: -0.1em;\n    padding-right: 5px;\n    opacity: 0.6;\n\n    @media (--mobile){\n      display: none;\n    }\n  }\n\n  &--active {\n    background: var(--color-background-icon-active);\n    color: var(--color-text-icon-active);\n  }\n\n  &--disabled {\n    color: var(--color-text-secondary);\n    cursor: default;\n    pointer-events: none;\n  }\n\n  &--focused {\n    &:not(.ce-popover-item--no-focus) {\n      background: var(--color-background-item-focus) !important;\n    }\n  }\n\n  &--hidden {\n    display: none;\n  }\n\n  @media (--can-hover) {\n    &:hover {\n      cursor: pointer;\n      \n      &:not(.ce-popover-item--no-hover) {\n        background-color: var(--color-background-item-hover);\n      }\n    }\n  }\n\n  &--confirmation {\n    background: var(--color-background-item-confirm);\n\n    .ce-popover-item__title,\n    .ce-popover-item__icon {\n      color: white;\n    }\n\n    /* confirmation hover */\n    &:not(.ce-popover-item--no-hover) {\n      @media (--can-hover) {\n        &:hover {\n          background: var(--color-background-item-confirm-hover);\n        }\n      }\n    }\n\n    /* confirmation focus */\n    &:not(.ce-popover-item--no-focus) {\n      &.ce-popover-item--focused {\n        background: var(--color-background-item-confirm-hover) !important;\n      }\n    }\n  }\n}\n\n\n/** \n * Animations\n */\n@keyframes panelShowing {\n  from {\n    opacity: 0;\n    transform: translateY(-8px) scale(0.9);\n  }\n\n  70% {\n    opacity: 1;\n    transform: translateY(2px);\n  }\n\n  to {\n\n    transform: translateY(0);\n  }\n}\n\n@keyframes panelShowingMobile {\n  from {\n    opacity: 0;\n    transform: translateY(14px) scale(0.98);\n  }\n\n  70% {\n    opacity: 1;\n    transform: translateY(-4px);\n  }\n\n  to {\n\n    transform: translateY(0);\n  }\n}\n\n\n.wobble {\n  animation-name: wobble;\n  animation-duration: 400ms;\n}\n\n/**\n * @author Nick Pettit - https://github.com/nickpettit/glide\n */\n@keyframes wobble {\n  from {\n    transform: translate3d(0, 0, 0);\n  }\n\n  15% {\n    transform: translate3d(-9%, 0, 0);\n  }\n\n  30% {\n    transform: translate3d(9%, 0, 0);\n  }\n\n  45% {\n    transform: translate3d(-4%, 0, 0);\n  }\n\n  60% {\n    transform: translate3d(4%, 0, 0);\n  }\n\n  75% {\n    transform: translate3d(-1%, 0, 0);\n  }\n\n  to {\n    transform: translate3d(0, 0, 0);\n  }\n}\n\n/**\n * Popover header styles\n */\n.ce-popover-header {\n  margin-bottom: 8px;\n  margin-top: 4px;\n  display: flex;\n  align-items: center;\n  \n  &__text {\n    font-size: 18px;\n    font-weight: 600;\n  }\n\n  &__back-button {\n    border: 0;\n    background: transparent;\n    width: 36px;\n    height: 36px;\n    color: var(--color-text-primary);\n\n    svg {\n      display: block;\n      width: 28px;\n      height: 28px;\n    }\n  }\n}\n"
  },
  {
    "path": "src/styles/rtl.css",
    "content": ".codex-editor.codex-editor--rtl {\n  direction: rtl;\n\n  .cdx-list {\n    padding-left: 0;\n    padding-right: 40px;\n  }\n\n  .ce-toolbar {\n    &__plus {\n      right: calc(var(--toolbox-buttons-size) * -1);\n      left: auto;\n    }\n\n    &__actions {\n      right: auto;\n      left: calc(var(--toolbox-buttons-size) * -1);\n\n      @media (--mobile){\n        margin-left: 0;\n        margin-right: auto;\n        padding-right: 0;\n        padding-left: 10px;\n      }\n    }\n  }\n\n  .ce-settings {\n    left: 5px;\n    right: auto;\n\n    &::before{\n      right: auto;\n      left: 25px;\n    }\n\n    &__button {\n      &:not(:nth-child(3n+3)) {\n        margin-left: 3px;\n        margin-right: 0;\n      }\n    }\n  }\n\n  .ce-conversion-tool {\n    &__icon {\n      margin-right: 0px;\n      margin-left: 10px;\n    }\n  }\n\n  .ce-inline-toolbar {\n    &__dropdown {\n      border-right: 0px solid transparent;\n      border-left: 1px solid var(--color-gray-border);\n      margin: 0 -6px 0 6px;\n\n      .icon--toggler-down {\n        margin-left: 0px;\n        margin-right: 4px;\n      }\n    }\n  }\n\n}\n\n.codex-editor--narrow.codex-editor--rtl {\n  .ce-toolbar__plus {\n    @media (--not-mobile) {\n      left: 0px;\n      right: 5px;\n    }\n  }\n\n  .ce-toolbar__actions {\n    @media (--not-mobile) {\n      left: -5px;\n    }\n  }\n}\n\n\n"
  },
  {
    "path": "src/styles/stub.css",
    "content": ".ce-stub {\n  display: flex;\n  align-items: center;\n  padding: 12px 18px;\n  margin: 10px 0;\n  border-radius: 10px;\n  background: var(--bg-light);\n  border: 1px solid var(--color-line-gray);\n  color: var(--grayText);\n  font-size: 14px;\n\n  svg {\n    width: var(--icon-size);\n    height: var(--icon-size);\n  }\n\n  &__info {\n    margin-left: 14px;\n  }\n\n  &__title {\n    font-weight: 500;\n    text-transform: capitalize;\n  }\n}\n"
  },
  {
    "path": "src/styles/toolbar.css",
    "content": ".ce-toolbar {\n  position: absolute;\n  left: 0;\n  right: 0;\n  top: 0;\n  transition: opacity 100ms ease;\n  will-change: opacity, top;\n\n  display: none;\n\n  &--opened {\n    display: block;\n  }\n\n  &__content {\n    max-width: var(--content-width);\n    margin: 0 auto;\n    position: relative;\n  }\n\n  &__plus {\n    @apply --toolbox-button;\n    flex-shrink: 0;\n\n    &-shortcut {\n      opacity: 0.6;\n      word-spacing: -2px;\n      margin-top: 5px;\n    }\n\n    @media (--mobile){\n      @apply --overlay-pane;\n      position: static;\n    }\n  }\n\n  /**\n   * Block actions Zone\n   * -------------------------\n   */\n  &__actions {\n    position: absolute;\n    right: 100%;\n    opacity: 0;\n    display: flex;\n    padding-right: 5px;\n\n    &--opened {\n      opacity: 1;\n    }\n\n    @media (--mobile){\n      right: auto;\n    }\n  }\n\n  &__settings-btn {\n    @apply --toolbox-button;\n\n    margin-left: 3px;\n    cursor: pointer;\n    user-select: none;\n\n    @media (--not-mobile){\n      width: 24px;\n    }\n\n    &--hidden {\n      display: none;\n    }\n\n    @media (--mobile){\n      @apply --overlay-pane;\n      position: static;\n    }\n  }\n\n  &__plus,\n  &__settings-btn {\n    svg {\n      width: 24px;\n      height: 24px;\n    }\n  }\n}\n\n/**\n * Styles for Narrow mode\n */\n.codex-editor--narrow .ce-toolbar__plus {\n  @media (--not-mobile) {\n    left: 5px;\n  }\n}\n"
  },
  {
    "path": "src/styles/toolbox.css",
    "content": ".ce-toolbox {\n\n}\n\n.codex-editor--narrow .ce-toolbox {\n  @media (--not-mobile){\n    .ce-popover {\n      right: 0;\n      left: unset;\n    }\n  }\n}\n"
  },
  {
    "path": "src/styles/ui.css",
    "content": "/**\n* Editor wrapper\n*/\n.codex-editor {\n  position: relative;\n  box-sizing: border-box;\n  z-index: 1;\n\n  .hide {\n    display: none;\n  }\n\n  &__redactor {\n    /**\n     * Workaround firefox bug: empty content editable elements has collapsed height\n     * https://bugzilla.mozilla.org/show_bug.cgi?id=1098151#c18\n     */\n    [contenteditable]:empty::after {\n      content: \"\\feff \";\n    }\n  }\n\n  /**\n   * Styles for narrow holder\n   */\n  &--narrow &__redactor {\n    @media (--not-mobile) {\n      margin-right: var(--narrow-mode-right-padding);\n    }\n  }\n\n  &--narrow&--rtl &__redactor {\n    @media (--not-mobile) {\n      margin-left: var(--narrow-mode-right-padding);\n      margin-right: 0;\n    }\n  }\n\n  &--narrow .ce-toolbar__actions {\n    @media (--not-mobile) {\n      right: -5px;\n    }\n  }\n\n  &-copyable {\n    position: absolute;\n    height: 1px;\n    width: 1px;\n    top: -400%;\n    opacity: 0.001;\n  }\n\n  &-overlay {\n    position: fixed;\n    top: 0px;\n    left: 0px;\n    right: 0px;\n    bottom: 0px;\n    z-index: 999;\n    pointer-events: none;\n    overflow: hidden;\n\n    &__container {\n      position: relative;\n      pointer-events: auto;\n      z-index: 0;\n    }\n\n    &__rectangle {\n       position: absolute;\n       pointer-events: none;\n       background-color: rgba(46, 170, 220, 0.2);\n       border: 1px solid transparent;\n    }\n  }\n\n  svg {\n    max-height: 100%;\n  }\n\n  path {\n    stroke: currentColor;\n  }\n\n\n  /**\n  * Set color for native selection\n  */\n  ::selection{\n    background-color: var(--inlineSelectionColor);\n  }\n}\n\n\n.codex-editor--toolbox-opened [contentEditable=true][data-placeholder]:focus::before {\n  opacity: 0 !important;\n}\n\n.ce-scroll-locked {\n  overflow: hidden;\n}\n\n.ce-scroll-locked--hard {\n  overflow: hidden;\n  top: calc(-1 * var(--window-scroll-offset));\n  position: fixed;\n  width: 100%;\n}\n"
  },
  {
    "path": "src/styles/variables.css",
    "content": "/**\n * Updating values in media queries should also include changes in utils.ts@isMobile\n */\n@custom-media --mobile (width <= 650px);\n@custom-media --not-mobile (width >= 651px);\n@custom-media --can-hover (hover: hover);\n\n:root {\n  /**\n   * Selection color\n   */\n  --selectionColor: #e1f2ff;\n  --inlineSelectionColor: #d4ecff;\n\n  /**\n   * Toolbar buttons\n   */\n  --bg-light: #eff2f5;\n\n  /**\n   * All gray texts: placeholders, settings\n   */\n  --grayText: #707684;\n\n  /**\n   * Gray icons hover\n   */\n  --color-dark: #1D202B;\n\n  /**\n   * Blue icons\n   */\n  --color-active-icon: #388AE5;\n\n  /**\n   * Gray border, loaders\n   * @deprecated — use --color-line-gray instead\n   */\n  --color-gray-border: rgba(201, 201, 204, 0.48);\n\n  /**\n   * Block content width\n   * Should be set in a constant at the modules/ui.js\n   */\n  --content-width: 650px;\n\n  /**\n   * In narrow mode, we increase right zone contained Block Actions button\n   */\n  --narrow-mode-right-padding: 50px;\n\n  /**\n   * Toolbar Plus Button and Toolbox buttons height and width\n   */\n  --toolbox-buttons-size: 26px;\n  --toolbox-buttons-size--mobile: 36px;\n\n  /**\n   * Size of svg icons got from the CodeX Icons pack\n   */\n  --icon-size: 20px;\n  --icon-size--mobile: 28px;\n\n\n  /**\n   * The main `.cdx-block` wrapper has such vertical paddings\n   * And the Block Actions toggler too\n   */\n  --block-padding-vertical: 0.4em;\n\n  --color-line-gray: #EFF0F1;\n\n  --overlay-pane {\n    position: absolute;\n    background-color: #FFFFFF;\n    border: 1px solid #E8E8EB;\n    box-shadow: 0 3px 15px -3px rgba(13,20,33,0.13);\n    border-radius: 6px;\n    z-index: 2;\n\n    &--left-oriented {\n      &::before {\n        left: 15px;\n        margin-left: 0;\n      }\n    }\n\n    &--right-oriented {\n      &::before {\n        left: auto;\n        right: 15px;\n        margin-left: 0;\n      }\n    }\n  };\n\n  --button-focused {\n    box-shadow: inset 0 0 0px 1px rgba(7, 161, 227, 0.08);\n    background: rgba(34, 186, 255, 0.08) !important;\n  };\n\n  --button-active {\n    background: rgba(56, 138, 229, 0.1);\n    color:  var(--color-active-icon);\n  };\n\n  --button-disabled {\n    color: var(--grayText);\n    cursor: default;\n    pointer-events: none;\n  }\n\n  /**\n   * Styles for Toolbox Buttons and Plus Button\n   */\n  --toolbox-button {\n    color: var(--color-dark);\n    cursor: pointer;\n    width: var(--toolbox-buttons-size);\n    height: var(--toolbox-buttons-size);\n    border-radius: 7px;\n    display: inline-flex;\n    justify-content: center;\n    align-items: center;\n    user-select: none;\n\n    @media (--mobile){\n      width: var(--toolbox-buttons-size--mobile);\n      height: var(--toolbox-buttons-size--mobile);\n    }\n\n    @media (--can-hover) {\n      &:hover {\n        background-color: var(--bg-light);\n      }\n    }\n\n    &--active {\n      background-color: var(--bg-light);\n      animation: bounceIn 0.75s 1;\n      animation-fill-mode: forwards;\n    }\n  };\n\n  /**\n   * Tool icon with border\n   */\n  --tool-icon {\n    display: inline-flex;\n    width: var(--toolbox-buttons-size);\n    height: var(--toolbox-buttons-size);\n    box-shadow: 0 0 0 1px var(--color-gray-border);\n    border-radius: 5px;\n    align-items: center;\n    justify-content: center;\n    background: #fff;\n    box-sizing: content-box;\n    flex-shrink: 0;\n    margin-right: 10px;\n\n    svg {\n      width: var(--icon-size);\n      height: var(--icon-size);\n    }\n\n    @media (--mobile) {\n      width: var(--toolbox-buttons-size--mobile);\n      height: var(--toolbox-buttons-size--mobile);\n      border-radius: 8px;\n\n      svg {\n        width: var(--icon-size--mobile);\n        height: var(--icon-size--mobile);\n      }\n    }\n  }\n}\n\n"
  },
  {
    "path": "src/tools/stub/index.ts",
    "content": "import $ from '../../components/dom';\nimport type { API, BlockTool, BlockToolConstructorOptions, BlockToolData } from '../../../types';\nimport { IconWarning } from '@codexteam/icons';\n\nexport interface StubData extends BlockToolData {\n  title: string;\n  savedData: BlockToolData;\n}\n\n/**\n * This tool will be shown in place of a block without corresponding plugin\n * It will store its data inside and pass it back with article saving\n */\nexport default class Stub implements BlockTool {\n  /**\n   * Notify core that tool supports read-only mode\n   */\n  public static isReadOnlySupported = true;\n\n  /**\n   * Stub styles\n   *\n   * @type {{wrapper: string, info: string, title: string, subtitle: string}}\n   */\n  private CSS = {\n    wrapper: 'ce-stub',\n    info: 'ce-stub__info',\n    title: 'ce-stub__title',\n    subtitle: 'ce-stub__subtitle',\n  };\n\n  /**\n   * Main stub wrapper\n   */\n  private readonly wrapper: HTMLElement;\n\n  /**\n   * Editor.js API\n   */\n  private readonly api: API;\n\n  /**\n   * Stub title — tool name\n   */\n  private readonly title: string;\n\n  /**\n   * Stub hint\n   */\n  private readonly subtitle: string;\n\n  /**\n   * Original Tool data\n   */\n  private readonly savedData: BlockToolData;\n\n  /**\n   * @param options - constructor options\n   * @param options.data - stub tool data\n   * @param options.api - Editor.js API\n   */\n  constructor({ data, api }: BlockToolConstructorOptions<StubData>) {\n    this.api = api;\n    this.title = data.title || this.api.i18n.t('Error');\n    this.subtitle = this.api.i18n.t('The block can not be displayed correctly.');\n    this.savedData = data.savedData;\n\n    this.wrapper = this.make();\n  }\n\n  /**\n   * Returns stub holder\n   *\n   * @returns {HTMLElement}\n   */\n  public render(): HTMLElement {\n    return this.wrapper;\n  }\n\n  /**\n   * Return original Tool data\n   *\n   * @returns {BlockToolData}\n   */\n  public save(): BlockToolData {\n    return this.savedData;\n  }\n\n  /**\n   * Create Tool html markup\n   *\n   * @returns {HTMLElement}\n   */\n  private make(): HTMLElement {\n    const wrapper = $.make('div', this.CSS.wrapper);\n    const icon = IconWarning;\n    const infoContainer = $.make('div', this.CSS.info);\n    const title = $.make('div', this.CSS.title, {\n      textContent: this.title,\n    });\n    const subtitle = $.make('div', this.CSS.subtitle, {\n      textContent: this.subtitle,\n    });\n\n    wrapper.innerHTML = icon;\n\n    infoContainer.appendChild(title);\n    infoContainer.appendChild(subtitle);\n\n    wrapper.appendChild(infoContainer);\n\n    return wrapper;\n  }\n}\n"
  },
  {
    "path": "src/types-internal/editor-modules.d.ts",
    "content": "/** ./api */\nimport BlocksAPI from '../components/modules/api/blocks';\nimport CaretAPI from '../components/modules/api/caret';\nimport EventsAPI from '../components/modules/api/events';\nimport I18nAPI from '../components/modules/api/i18n';\nimport API from '../components/modules/api/index';\nimport InlineToolbarAPI from '../components/modules/api/inlineToolbar';\nimport ListenersAPI from '../components/modules/api/listeners';\nimport NotifierAPI from '../components/modules/api/notifier';\nimport ReadOnlyAPI from '../components/modules/api/readonly';\nimport SanitizerAPI from '../components/modules/api/sanitizer';\nimport SaverAPI from '../components/modules/api/saver';\nimport SelectionAPI from '../components/modules/api/selection';\nimport StylesAPI from '../components/modules/api/styles';\nimport ToolbarAPI from '../components/modules/api/toolbar';\nimport TooltipAPI from '../components/modules/api/tooltip';\nimport UiAPI from '../components/modules/api/ui';\n\n/** ./toolbar */\nimport BlockSettings from '../components/modules/toolbar/blockSettings';\nimport Toolbar from '../components/modules/toolbar/index';\nimport InlineToolbar from '../components/modules/toolbar/inline';\n\n/** . */\nimport BlockEvents from '../components/modules/blockEvents';\nimport BlockManager from '../components/modules/blockManager';\nimport BlockSelection from '../components/modules/blockSelection';\nimport Caret from '../components/modules/caret';\nimport CrossBlockSelection from '../components/modules/crossBlockSelection';\nimport DragNDrop from '../components/modules/dragNDrop';\nimport ModificationsObserver from '../components/modules/modificationsObserver';\nimport Paste from '../components/modules/paste';\nimport ReadOnly from '../components/modules/readonly';\nimport RectangleSelection from '../components/modules/rectangleSelection';\nimport Renderer from '../components/modules/renderer';\nimport Saver from '../components/modules/saver';\nimport Tools from '../components/modules/tools';\nimport UI from '../components/modules/ui';\nimport ToolsAPI from '../components/modules/api/tools';\n\nexport interface EditorModules {\n  // API Modules\n  BlocksAPI: BlocksAPI,\n  CaretAPI: CaretAPI,\n  ToolsAPI: ToolsAPI,\n  EventsAPI: EventsAPI,\n  I18nAPI: I18nAPI,\n  API: API,\n  InlineToolbarAPI: InlineToolbarAPI,\n  ListenersAPI: ListenersAPI,\n  NotifierAPI: NotifierAPI,\n  ReadOnlyAPI: ReadOnlyAPI,\n  SanitizerAPI: SanitizerAPI,\n  SaverAPI: SaverAPI,\n  SelectionAPI: SelectionAPI,\n  StylesAPI: StylesAPI,\n  ToolbarAPI: ToolbarAPI,\n  TooltipAPI: TooltipAPI,\n  UiAPI: UiAPI,\n\n  // Toolbar Modules\n  BlockSettings: BlockSettings,\n  Toolbar: Toolbar,\n  InlineToolbar: InlineToolbar,\n\n  // Modules\n  BlockEvents: BlockEvents,\n  BlockManager: BlockManager,\n  BlockSelection: BlockSelection,\n  Caret: Caret,\n  CrossBlockSelection: CrossBlockSelection,\n  DragNDrop: DragNDrop,\n  ModificationsObserver: ModificationsObserver,\n  Paste: Paste,\n  ReadOnly: ReadOnly,\n  RectangleSelection: RectangleSelection,\n  Renderer: Renderer,\n  Saver: Saver,\n  Tools: Tools,\n  UI: UI,\n}\n"
  },
  {
    "path": "src/types-internal/html-janitor.d.ts",
    "content": "/**\n * Declaration for external JS module\n * After that we can use it at the TS modules\n */\ndeclare module 'html-janitor' {\n  /**\n   * Sanitizer config of each HTML element\n   * @see {@link https://github.com/guardian/html-janitor#options}\n   */\n  type TagConfig = boolean | { [attr: string]: boolean | string };\n\n  interface Config {\n    tags: {\n      [key: string]: TagConfig | ((el: Element) => TagConfig)\n    };\n  }\n\n  export class HTMLJanitor {\n    constructor(config: Config);\n\n    public clean(taintString: string): string;\n  }\n\n  /**\n   * Default export\n   */\n  export default HTMLJanitor;\n}\n"
  },
  {
    "path": "src/types-internal/i18n-internal-namespace.d.ts",
    "content": "/**\n* Decorator above the type object\n*/\ntype Indexed<T> = { [key: string]: T };\n\n/**\n * Type for I18n dictionary values that can be strings or dictionary sub-sections\n *\n * Can be used as:\n *   LeavesDictKeys<typeof myDictionary>\n *\n * where myDictionary is a JSON with messages\n */\nexport type LeavesDictKeys<D> = D extends string\n  /**\n   * If generic type is string, just return it\n   */\n  ? D\n  /**\n   * If generic type is object that has only one level and contains only strings, return it's keys union\n   *\n   * { key: \"string\", anotherKey: \"string\" } => \"key\" | \"anotherKey\"\n   *\n   */\n  : D extends Indexed<string>\n    ? keyof D\n    /**\n     * If generic type is object, but not the one described above,\n     * use LeavesDictKey on it's values recursively and union the results\n     *\n     * { \"rootKey\": { \"subKey\": \"string\" }, \"anotherRootKey\": { \"anotherSubKey\": \"string\" } } => \"subKey\" | \"anotherSubKey\"\n     *\n     */\n    : D extends Indexed<any>\n      ? { [K in keyof D]: LeavesDictKeys<D[K]> }[keyof D]\n\n      /**\n       * In other cases, return never type\n       */\n      : never;\n\n/**\n * Provide type-safe access to the available namespaces of the dictionary\n *\n * Can be uses as:\n *    DictNamespaces<typeof myDictionary>\n *\n * where myDictionary is a JSON with messages\n */\nexport type DictNamespaces<D extends object> = {\n  /**\n   * Iterate through generic type keys\n   *\n   * If value under current key is object that has only one level and contains only strings, return string type\n   */\n  [K in keyof D]: D[K] extends Indexed<string>\n    ? string\n    /**\n     * If value under current key is object with depth more than one, apply DictNamespaces recursively\n     */\n    : D[K] extends Indexed<any>\n      ? DictNamespaces<D[K]>\n      /**\n       * In other cases, return never type\n       */\n      : never;\n}\n\n"
  },
  {
    "path": "src/types-internal/module-config.d.ts",
    "content": "import { EditorConfig } from '../../types/index';\nimport { EditorEventMap } from '../components/events';\nimport EventsDispatcher from '../components/utils/events';\n\n/**\n * Describes object passed to Editor modules constructor\n */\nexport interface ModuleConfig {\n  config: EditorConfig;\n  eventsDispatcher: EventsDispatcher<EditorEventMap>;\n}\n"
  },
  {
    "path": "test/cypress/.eslintrc",
    "content": "{\n    \"plugins\": [\n        \"cypress\",\n        \"chai-friendly\"\n    ],\n    \"env\": {\n        \"cypress/globals\": true\n    },\n    \"extends\": [\n        \"plugin:cypress/recommended\",\n        \"plugin:chai-friendly/recommended\"\n    ],\n    \"rules\": {\n        \"cypress/require-data-selectors\": 2,\n        \"cypress/no-unnecessary-waiting\": 0,\n        \"@typescript-eslint/no-magic-numbers\": 0\n    },\n    \"globals\": {\n        \"EditorJS\": true\n    }\n}\n"
  },
  {
    "path": "test/cypress/fixtures/test.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<body>\n  <!-- Load Editor.js's Core -->\n  <script src=\"./../../../dist/editorjs.umd.js\"></script>\n  <h1>Editor.js test page</h1>\n</body>\n</html>\n"
  },
  {
    "path": "test/cypress/fixtures/tools/ContentlessTool.ts",
    "content": "import type { BlockTool } from '../../../../types';\n\n/**\n * In the simplest Contentless Tool (eg. Delimiter) there is no data to save\n */\ninterface ContentlessToolData {}\n\n/**\n * This tool behaves like a delimiter\n */\nexport default class ContentlessToolMock implements BlockTool {\n  /**\n   * Renders a single content editable element as tools element\n   */\n  public render(): HTMLElement {\n    const wrapper = document.createElement('div');\n\n    wrapper.dataset.cyType = 'contentless-tool';\n\n    wrapper.textContent = '***';\n\n    return wrapper;\n  }\n\n  /**\n   * Save method mock\n   */\n  public save(): ContentlessToolData {\n    return {};\n  }\n\n  /**\n   * Allow Tool to have no content\n   */\n  public static get contentless(): boolean {\n    return true;\n  }\n}\n"
  },
  {
    "path": "test/cypress/fixtures/tools/SimpleHeader.ts",
    "content": "import type {\n  BaseTool,\n  BlockToolConstructorOptions,\n  BlockToolData,\n  ConversionConfig\n} from '../../../../types';\n\n/**\n * Simplified Header for testing\n */\nexport class SimpleHeader implements BaseTool {\n  private _data: BlockToolData;\n  private element: HTMLHeadingElement | null = null;\n\n  /**\n   *\n   * @param options - constructor options\n   */\n  constructor({ data }: BlockToolConstructorOptions) {\n    this._data = data;\n  }\n\n  /**\n   * Return Tool's view\n   *\n   * @returns {HTMLHeadingElement}\n   * @public\n   */\n  public render(): HTMLHeadingElement {\n    this.element = document.createElement('h1');\n\n    this.element.contentEditable = 'true';\n    this.element.innerHTML = this._data.text;\n\n    return this.element;\n  }\n\n  /**\n   * @param data - saved data to merger with current block\n   */\n  public merge(data: BlockToolData): void {\n    this.element?.insertAdjacentHTML('beforeend', data.text);\n  }\n\n  /**\n   * Extract Tool's data from the view\n   *\n   * @param toolsContent - Text tools rendered view\n   */\n  public save(toolsContent: HTMLHeadingElement): BlockToolData {\n    return {\n      text: toolsContent.innerHTML,\n      level: 1,\n    };\n  }\n\n  /**\n   * Allow Header to be converted to/from other blocks\n   */\n  public static get conversionConfig(): ConversionConfig {\n    return {\n      export: 'text', // use 'text' property for other blocks\n      import: 'text', // fill 'text' property from other block's export string\n    };\n  }\n}\n"
  },
  {
    "path": "test/cypress/fixtures/tools/ToolMock.ts",
    "content": "import type { BlockTool, BlockToolConstructorOptions } from '../../../../types';\n\n/**\n * Simple structure for Tool data\n */\nexport interface MockToolData {\n  text: string;\n}\n\n/**\n * Common class for Tool mocking.\n * Extend this class to create a mock for your Tool with specific properties.\n */\nexport default class ToolMock implements BlockTool {\n  /**\n   * Tool data\n   */\n  private data: MockToolData;\n\n  /**\n   * Creates new Tool instance\n   *\n   * @param options - tool constructor options\n   */\n  constructor(options: BlockToolConstructorOptions<MockToolData>) {\n    this.data = options.data;\n  }\n\n  /**\n   * Renders a single content editable element as tools element\n   */\n  public render(): HTMLElement {\n    const contenteditable = document.createElement('div');\n\n    if (this.data && this.data.text) {\n      contenteditable.innerHTML = this.data.text;\n    }\n\n    contenteditable.contentEditable = 'true';\n\n    return contenteditable;\n  }\n\n  /**\n   * Save method mock, returns block innerHTML\n   *\n   * @param block - element rendered by the render method\n   */\n  public save(block: HTMLElement): MockToolData {\n    return {\n      text: block.innerHTML,\n    };\n  }\n}\n"
  },
  {
    "path": "test/cypress/fixtures/tools/ToolWithoutConversionExport.ts",
    "content": "import type { ConversionConfig } from '@/types/configs/conversion-config';\nimport ToolMock from './ToolMock';\n\n/**\n * This tool has a conversionConfig, but it doesn't have export property.\n *\n * That means that tool can be created from string, but can't be converted to string.\n */\nexport class ToolWithoutConversionExport extends ToolMock {\n  /**\n   * Rules specified how our Tool can be converted to/from other Tool.\n   */\n  public static get conversionConfig(): ConversionConfig {\n    return {\n      import: 'text', // this tool can be created from string\n\n      /**\n       * Here is no \"export\" property, so this tool can't be converted to string\n       */\n      // export: (data) => data.text,\n    };\n  }\n}\n"
  },
  {
    "path": "test/cypress/fixtures/types/PartialBlockMutationEvent.ts",
    "content": "import type { BlockMutationEvent, BlockMutationType } from '../../../../types';\n\n/**\n * Simplified version of the BlockMutationEvent with optional fields that could be used in tests\n */\nexport default interface PartialBlockMutationEvent {\n  /**\n   * Event type\n   */\n  type?: BlockMutationType,\n\n  /**\n   * Details with partial properties\n   */\n  detail?: Partial<BlockMutationEvent['detail']>\n}\n"
  },
  {
    "path": "test/cypress/support/commands.ts",
    "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\n/**\n * This file contains custom commands for Cypress.\n * Also it can override the existing commands.\n *\n * --------------------------------------------------\n */\n\nimport type { EditorConfig, OutputData } from './../../../types/index';\nimport type EditorJS from '../../../types/index';\nimport Chainable = Cypress.Chainable;\n\n/**\n * Create a wrapper and initialize the new instance of editor.js\n * Then return the instance\n *\n * @param editorConfig - config to pass to the editor\n * @returns EditorJS - created instance\n */\nCypress.Commands.add('createEditor', (editorConfig: EditorConfig = {}): Chainable<EditorJS> => {\n  return cy.window()\n    .then((window) => {\n      return new Promise((resolve: (instance: EditorJS) => void) => {\n        const editorContainer = window.document.createElement('div');\n\n        editorContainer.setAttribute('id', 'editorjs');\n        editorContainer.dataset.cy = 'editorjs';\n        editorContainer.style.border = '1px dotted #388AE5';\n\n        window.document.body.appendChild(editorContainer);\n\n        const editorInstance: EditorJS = new window.EditorJS(editorConfig);\n\n        editorInstance.isReady.then(() => {\n          resolve(editorInstance);\n        });\n      });\n    });\n});\n\n/**\n * Paste command to dispatch paste event\n *\n * Usage\n * cy.get('div').paste({'text/plain': 'Text', 'text/html': '<b>Text</b>'})\n *\n * @param data - map with MIME type as a key and data as value\n */\nCypress.Commands.add('paste', {\n  prevSubject: true,\n}, (subject, data: {[type: string]: string}) => {\n  const pasteEvent = Object.assign(new Event('paste', {\n    bubbles: true,\n    cancelable: true,\n  }), {\n    clipboardData: {\n      getData: (type): string => data[type],\n      types: Object.keys(data),\n    },\n  });\n\n  subject[0].dispatchEvent(pasteEvent);\n\n  cy.wait(200); // wait a little since some tools (paragraph) could have async hydration\n});\n\n/**\n * Copy command to dispatch copy event on subject\n *\n * Usage:\n * cy.get('div').copy().then(data => {})\n */\nCypress.Commands.add('copy', { prevSubject: true }, (subject) => {\n  const clipboardData: {[type: string]: any} = {};\n\n  const copyEvent = Object.assign(new Event('copy', {\n    bubbles: true,\n    cancelable: true,\n  }), {\n    clipboardData: {\n      setData: (type: string, data: any): void => {\n        clipboardData[type] = data;\n      },\n    },\n  });\n\n  subject[0].dispatchEvent(copyEvent);\n\n  return cy.wrap(clipboardData);\n});\n\n/**\n * Cut command to dispatch cut event on subject\n *\n * Usage:\n * cy.get('div').cut().then(data => {})\n */\nCypress.Commands.add('cut', { prevSubject: true }, (subject) => {\n  const clipboardData: {[type: string]: any} = {};\n\n  const copyEvent = Object.assign(new Event('cut', {\n    bubbles: true,\n    cancelable: true,\n  }), {\n    clipboardData: {\n      setData: (type: string, data: any): void => {\n        clipboardData[type] = data;\n      },\n    },\n  });\n\n  subject[0].dispatchEvent(copyEvent);\n\n  return cy.wrap(clipboardData);\n});\n\n/**\n * Calls EditorJS API render method\n *\n * @param data — data to render\n */\nCypress.Commands.add('render', { prevSubject: true }, (subject: EditorJS, data: OutputData) => {\n  return cy.wrap(subject.render(data))\n    .then(() => {\n      return cy.wrap(subject);\n    });\n});\n\n\n/**\n * Select passed text in element\n * Note. Previous subject should have 'textNode' as firstChild\n *\n * Usage\n * cy.get('[data-cy=editorjs]')\n *  .find('.ce-paragraph')\n *  .selectText('block te')\n *\n * @param text - text to select\n */\nCypress.Commands.add('selectText', {\n  prevSubject: true,\n}, (subject, text: string) => {\n  const el = subject[0];\n  const document = el.ownerDocument;\n  const range = document.createRange();\n  const textNode = el.firstChild;\n  const selectionPositionStart = textNode.textContent.indexOf(text);\n  const selectionPositionEnd = selectionPositionStart + text.length;\n\n  range.setStart(textNode, selectionPositionStart);\n  range.setEnd(textNode, selectionPositionEnd);\n  document.getSelection().removeAllRanges();\n  document.getSelection().addRange(range);\n\n  return cy.wrap(subject);\n});\n\n/**\n * Select element's text by offset\n * Note. Previous subject should have 'textNode' as firstChild\n *\n * Usage\n * cy.get('[data-cy=editorjs]')\n *  .find('.ce-paragraph')\n *  .selectTextByOffset([0, 5])\n *\n * @param offset - offset to select\n */\nCypress.Commands.add('selectTextByOffset', {\n  prevSubject: true,\n}, (subject, offset: [number, number]) => {\n  const el = subject[0];\n  const document = el.ownerDocument;\n  const range = document.createRange();\n  const textNode = el.firstChild;\n  const selectionPositionStart = offset[0];\n  const selectionPositionEnd = offset[1];\n\n  range.setStart(textNode, selectionPositionStart);\n  range.setEnd(textNode, selectionPositionEnd);\n  document.getSelection().removeAllRanges();\n  document.getSelection().addRange(range);\n\n  return cy.wrap(subject);\n});\n\n/**\n * Returns line wrap positions for passed element\n *\n * Usage\n * cy.get('[data-cy=editorjs]')\n *  .find('.ce-paragraph')\n *  .getLineWrapPositions()\n *\n * @returns number[] - array of line wrap positions\n */\nCypress.Commands.add('getLineWrapPositions', {\n  prevSubject: true,\n}, (subject) => {\n  const element = subject[0];\n  const document = element.ownerDocument;\n  const text = element.textContent;\n  const lineWraps = [];\n\n  let currentLineY = 0;\n\n  /**\n   * Iterate all chars in text, create range for each char and get its position\n   */\n  for (let i = 0; i < text.length; i++) {\n    const range = document.createRange();\n\n    range.setStart(element.firstChild, i);\n    range.setEnd(element.firstChild, i);\n\n    const rect = range.getBoundingClientRect();\n\n    if (i === 0) {\n      currentLineY = rect.top;\n\n      continue;\n    }\n\n    /**\n     * If current char Y position is higher than previously saved line Y, that means a line wrap\n     */\n    if (rect.top > currentLineY) {\n      lineWraps.push(i);\n\n      currentLineY = rect.top;\n    }\n  }\n\n  return cy.wrap(lineWraps);\n});\n\n/**\n * Dispatches keydown event on subject\n * Uses the correct KeyboardEvent object to make it work with our code (see below)\n */\nCypress.Commands.add('keydown', {\n  prevSubject: true,\n}, (subject, keyCode: number) => {\n  cy.log('Dispatching KeyboardEvent with keyCode: ' + keyCode);\n  /**\n   * We use the \"reason instanceof KeyboardEvent\" statement in blockSelection.ts\n   * but by default cypress' KeyboardEvent is not an instance of the native KeyboardEvent,\n   * so real-world and Cypress behaviour were different.\n   *\n   * To make it work we need to trigger Cypress event with \"eventConstructor: 'KeyboardEvent'\",\n   *\n   * @see https://github.com/cypress-io/cypress/issues/5650\n   * @see https://github.com/cypress-io/cypress/pull/8305/files\n   */\n  subject.trigger('keydown', {\n    eventConstructor: 'KeyboardEvent',\n    keyCode,\n    bubbles: false,\n  });\n\n  return cy.wrap(subject);\n});\n\n/**\n * Extract content of pseudo element\n *\n * @example cy.get('element').getPseudoElementContent('::before').should('eq', 'my-test-string')\n */\nCypress.Commands.add('getPseudoElementContent', {\n  prevSubject: true,\n}, (subject, pseudoElement: 'string') => {\n  const win = subject[0].ownerDocument.defaultView;\n  const computedStyle = win.getComputedStyle(subject[0], pseudoElement);\n  const content = computedStyle.getPropertyValue('content');\n\n  return content.replace(/['\"]/g, ''); // Remove quotes around the content\n});\n"
  },
  {
    "path": "test/cypress/support/e2e.ts",
    "content": "import '@cypress/code-coverage/support';\n\n/* global chai */\n// because this file is imported from cypress/support/e2e.js\n// that means all other spec files will have this assertion plugin\n// available to them because the supportFile is bundled and served\n// prior to any spec files loading\n\nimport type PartialBlockMutationEvent from '../fixtures/types/PartialBlockMutationEvent';\n\n/**\n * Chai plugin for checking if passed onChange method is called with an array of passed events\n *\n * @param _chai - Chai instance\n */\nconst beCalledWithBatchedEvents = (_chai): void => {\n  /**\n   * Check if passed onChange method is called with an array of passed events\n   *\n   * @param expectedEvents - batched events to check\n   */\n  function assertToBeCalledWithBatchedEvents(expectedEvents: PartialBlockMutationEvent[]): void {\n    /**\n     * EditorJS API is passed as the first parameter of the onChange callback\n     */\n    const EditorJSApiMock = Cypress.sinon.match.any;\n    const $onChange = this._obj;\n\n    this.assert(\n      $onChange.calledOnce,\n      'expected #{this} to be called once',\n      'expected #{this} to not be called once'\n    );\n\n    this.assert(\n      $onChange.calledWithMatch(\n        EditorJSApiMock,\n        Cypress.sinon.match((events: PartialBlockMutationEvent[]) => {\n          expect(events).to.be.an('array');\n\n          return events.every((event, index) => {\n            const eventToCheck = expectedEvents[index];\n\n            return expect(event).to.containSubset(eventToCheck);\n          });\n        })\n      ),\n      'expected #{this} to be called with #{exp}, but it was called with #{act}',\n      'expected #{this} to not be called with #{exp}, but it was called with #{act} ',\n      expectedEvents\n    );\n  }\n\n  _chai.Assertion.addMethod('calledWithBatchedEvents', assertToBeCalledWithBatchedEvents);\n};\n\n/**\n * registers our assertion function \"beCalledWithBatchedEvents\" with Chai\n */\nchai.use(beCalledWithBatchedEvents);\n"
  },
  {
    "path": "test/cypress/support/index.d.ts",
    "content": "// load type definitions that come with Cypress module\n/// <reference types=\"cypress\" />\n\nimport type { EditorConfig, OutputData } from './../../../types/index';\nimport type EditorJS from '../../../types/index'\nimport PartialBlockMutationEvent from '../fixtures/types/PartialBlockMutationEvent';\n\ndeclare global {\n  namespace Cypress {\n    interface Chainable<Subject = any> {\n      /**\n       * Custom command to select DOM element by data-cy attribute.\n       * @param editorConfig - config to pass to the editor\n       * @example cy.createEditor({})\n       */\n      createEditor(editorConfig: EditorConfig): Chainable<EditorJS>\n\n      /**\n       * Paste command to dispatch paste event\n       *\n       * @usage\n       * cy.get('div').paste({'text/plain': 'Text', 'text/html': '<b>Text</b>'})\n       *\n       * @param data - map with MIME type as a key and data as value\n       */\n      paste(data: {[type: string]: string}): Chainable<Subject>\n\n      /**\n       * Copy command to dispatch copy event on subject\n       *\n       * @usage\n       * cy.get('div').copy().then(data => {})\n       */\n      copy(): Chainable<Subject>;\n\n      /**\n       * Cut command to dispatch cut event on subject\n       *\n       * @usage\n       * cy.get('div').cut().then(data => {})\n       */\n      cut(): Chainable<Subject>;\n\n      /**\n       * Calls EditorJS API render method\n       *\n       * @param data — data to render\n       */\n      render(data: OutputData): Chainable<Subject>;\n\n      /**\n       * Select passed text in element\n       * Note. Previous subject should have 'textNode' as firstChild\n       *\n       * Usage\n       * cy.get('[data-cy=editorjs]')\n       *  .find('.ce-paragraph')\n       *  .selectText('block te')\n       *\n       * @param text - text to select\n       */\n      selectText(text: string): Chainable<Subject>;\n\n      /**\n       * Select element's text by offset\n       * Note. Previous subject should have 'textNode' as firstChild\n       *\n       * Usage\n       * cy.get('[data-cy=editorjs]')\n       *  .find('.ce-paragraph')\n       *  .selectTextByOffset([0, 5])\n       *\n       * @param offset - offset to select\n       */\n      selectTextByOffset(offset: [number, number]): Chainable<Subject>;\n\n      /**\n       * Returns line wrap positions for passed element\n       *\n       * Usage\n       * cy.get('[data-cy=editorjs]')\n       *  .find('.ce-paragraph')\n       *  .getLineWrapPositions()\n       *\n       * @returns number[] - array of line wrap positions\n       */\n      getLineWrapPositions(): Chainable<number[]>;\n\n      /**\n       * Dispatches keydown event on subject\n       * Uses the correct KeyboardEvent object to make it work with our code (see below)\n       *\n       * @param keyCode - key code to dispatch\n       */\n      keydown(keyCode: number): Chainable<Subject>;\n\n      /**\n       * Extract content of pseudo element\n       *\n       * @example cy.get('element').getPseudoElementContent('::before').should('eq', 'my-test-string')\n       */\n      getPseudoElementContent(pseudoElement: string): Chainable<string>;\n    }\n\n    interface ApplicationWindow {\n      EditorJS: typeof EditorJS\n    }\n\n    /**\n     * Extends Cypress assertion Chainer interface with the new assertion methods\n     */\n    interface Chainer<Subject> {\n      /**\n       * Custom Chai assertion that checks if given onChange method is called with an array of passed events\n       *\n       * @example\n       *   ```\n       *   cy.get('@onChange').should('be.calledWithBatchedEvents', [{ type: 'block-added', detail: { index: 0 }}])\n       *   expect(onChange).to.be.calledWithBatchedEvents([{ type: 'block-added', detail: { index: 0 }}])\n       *   ```\n       */\n      (chainer: 'be.calledWithBatchedEvents', expectedEvents: PartialBlockMutationEvent[]): Chainable<Subject>;\n    }\n  }\n\n  /**\n   * Chai plugins\n   */\n  namespace Chai {\n    interface Assertion {\n      /**\n       * \"containSubset\" object properties matcher\n       */\n      containSubset(subset: any): Assertion;\n\n      /**\n       * Custom Chai assertion that checks if given onChange method is called with an array of passed events\n       *\n       * @example\n       *   ```\n       *   cy.get('@onChange').should('be.calledWithBatchedEvents', [{ type: 'block-added', detail: { index: 0 }}])\n       *   expect(onChange).to.be.calledWithBatchedEvents([{ type: 'block-added', detail: { index: 0 }}])\n       *   ```\n       */\n      calledWithBatchedEvents(expectedEvents: PartialBlockMutationEvent[]): Assertion;\n    }\n  }\n}\n"
  },
  {
    "path": "test/cypress/support/index.ts",
    "content": "/**\n * This file is processed and\n * loaded automatically before the test files.\n *\n * This is a great place to put global configuration and\n * behavior that modifies Cypress.\n */\n\nimport '@cypress/code-coverage/support';\nimport installLogsCollector from 'cypress-terminal-report/src/installLogsCollector';\nimport 'cypress-plugin-tab';\n\ninstallLogsCollector();\n\n/**\n * File with the helpful commands\n */\nimport './commands';\n\n/**\n * File with custom assertions\n */\nimport './e2e';\n\nimport chaiSubset from 'chai-subset';\n\n/**\n * \"containSubset\" object properties matcher\n */\nchai.use(chaiSubset);\n\n/**\n * Before-each hook for the cypress tests\n */\nbeforeEach((): void => {\n  cy.visit('test/cypress/fixtures/test.html');\n});\n"
  },
  {
    "path": "test/cypress/support/utils/createEditorWithTextBlocks.ts",
    "content": "import type { EditorConfig } from '../../../../types/index';\nimport Chainable = Cypress.Chainable;\nimport type EditorJS from '../../../../types/index';\n\n\n/**\n * Creates Editor instance with list of Paragraph blocks of passed texts\n *\n * @param textBlocks - list of texts for Paragraph blocks\n * @param editorConfig - config to pass to the editor\n */\nexport function createEditorWithTextBlocks(textBlocks: string[], editorConfig?: Omit<EditorConfig, 'data'>): Chainable<EditorJS> {\n  return cy.createEditor(Object.assign(editorConfig || {}, {\n    data: {\n      blocks: textBlocks.map((text) => ({\n        type: 'paragraph',\n        data: {\n          text,\n        },\n      })),\n    },\n  }));\n}\n"
  },
  {
    "path": "test/cypress/support/utils/createParagraphMock.ts",
    "content": "import { nanoid } from 'nanoid';\n\n/**\n * Creates a paragraph mock\n *\n * @param text - text for the paragraph\n * @returns paragraph mock\n */\nexport function createParagraphMock(text: string): {\n  id: string;\n  type: string;\n  data: { text: string };\n} {\n  return {\n    id: nanoid(),\n    type: 'paragraph',\n    data: { text },\n  };\n}"
  },
  {
    "path": "test/cypress/support/utils/nestedEditorInstance.ts",
    "content": "import type { BlockTool, BlockToolConstructorOptions } from '../../../../types';\nimport { createEditorWithTextBlocks } from './createEditorWithTextBlocks';\n\nexport const NESTED_EDITOR_ID = 'nested-editor';\n\n/**\n * Creates nested Editor instance with paragraph block\n */\nexport default class NestedEditor implements BlockTool {\n  private data: { text: string };\n\n  constructor(value: BlockToolConstructorOptions) {\n    this.data = value.data;\n  }\n\n  public render(): HTMLDivElement {\n    const editorEl = Object.assign(document.createElement('div'), {\n      id: NESTED_EDITOR_ID,\n    });\n\n    editorEl.setAttribute('data-cy', NESTED_EDITOR_ID);\n\n    createEditorWithTextBlocks([ this.data.text ], { holder: NESTED_EDITOR_ID });\n\n    return editorEl;\n  }\n\n  public save(): string {\n    return this.data.text;\n  }\n}\n"
  },
  {
    "path": "test/cypress/tests/api/block.cy.ts",
    "content": "import type EditorJS from '../../../../types';\nimport { BlockChangedMutationType } from '../../../../types/events/block/BlockChanged';\n\n/**\n * There will be described test cases of BlockAPI\n */\ndescribe('BlockAPI', () => {\n  const firstBlock = {\n    id: 'bwnFX5LoX7',\n    type: 'paragraph',\n    data: {\n      text: 'The first block content mock.',\n    },\n  };\n  const editorDataMock = {\n    blocks: [\n      firstBlock,\n    ],\n  };\n\n  /**\n   * EditorJS API is passed as the first parameter of the onChange callback\n   */\n  const EditorJSApiMock = Cypress.sinon.match.any;\n\n  /**\n   * Creates Editor instance\n   *\n   * @param [data] - data to render\n   */\n  function createEditor(data = undefined): void {\n    const config = {\n      onChange: (api, event): void => {\n        console.log('something changed', event);\n      },\n      data,\n    };\n\n    cy.spy(config, 'onChange').as('onChange');\n\n    cy.createEditor(config).as('editorInstance');\n  }\n\n  /**\n   * block.dispatchChange();\n   */\n  describe('.dispatchChange()', () => {\n    /**\n     * Check that blocks.dispatchChange() triggers Editor 'onChange' callback\n     */\n    it('should trigger onChange with corresponded block', () => {\n      createEditor(editorDataMock);\n\n      cy.get<EditorJS>('@editorInstance')\n        .then(async (editor) => {\n          const block = editor.blocks.getById(firstBlock.id);\n\n          block.dispatchChange();\n\n          cy.get('@onChange').should('be.calledWithMatch', EditorJSApiMock, Cypress.sinon.match({\n            type: BlockChangedMutationType,\n            detail: {\n              index: 0,\n            },\n          }));\n        });\n    });\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/api/blocks.cy.ts",
    "content": "import type EditorJS from '../../../../types/index';\nimport type { ConversionConfig, ToolboxConfig, ToolConfig } from '../../../../types';\nimport ToolMock, { type MockToolData } from '../../fixtures/tools/ToolMock';\nimport { nanoid } from 'nanoid';\n\n/**\n * There will be described test cases of 'blocks.*' API\n */\ndescribe('api.blocks', () => {\n  const firstBlock = {\n    id: 'bwnFX5LoX7',\n    type: 'paragraph',\n    data: {\n      text: 'The first block content mock.',\n    },\n  };\n  const editorDataMock = {\n    blocks: [\n      firstBlock,\n    ],\n  };\n\n  /**\n   * api.blocks.getById(id)\n   */\n  describe('.getById()', () => {\n    /**\n     * Check that api.blocks.getByUd(id) returns the Block for existed id\n     */\n    it('should return Block API for existed id', () => {\n      cy.createEditor({\n        data: editorDataMock,\n      }).as('editorInstance');\n\n      cy.get<EditorJS>('@editorInstance').then(async (editor) => {\n        const block = editor.blocks.getById(firstBlock.id);\n\n        expect(block).not.to.be.undefined;\n        expect(block.id).to.be.eq(firstBlock.id);\n      });\n    });\n\n    /**\n     * Check that api.blocks.getByUd(id) returns null for the not-existed id\n     */\n    it('should return null for not-existed id', () => {\n      cy.createEditor({\n        data: editorDataMock,\n      }).as('editorInstance');\n\n      cy.get<EditorJS>('@editorInstance').then(async (editor) => {\n        expect(editor.blocks.getById('not-existed-id')).to.be.null;\n      });\n    });\n  });\n\n  /**\n   * api.blocks.update(id, newData)\n   */\n  describe('.update()', () => {\n    /**\n     * Check if block is updated in DOM\n     */\n    it('should update block in DOM', () => {\n      cy.createEditor({\n        data: editorDataMock,\n      }).then((editor) => {\n        const idToUpdate = firstBlock.id;\n        const newBlockData = {\n          text: 'Updated text',\n        };\n\n        editor.blocks.update(idToUpdate, newBlockData);\n\n        cy.get('[data-cy=editorjs]')\n          .get('div.ce-block')\n          .should('have.text', newBlockData.text);\n      });\n    });\n\n    /**\n     * Check if block's data is updated after saving\n     */\n    it('should update block in saved data', () => {\n      cy.createEditor({\n        data: editorDataMock,\n      }).then((editor) => {\n        const idToUpdate = firstBlock.id;\n        const newBlockData = {\n          text: 'Updated text',\n        };\n\n        editor.blocks.update(idToUpdate, newBlockData);\n\n        // wait a little since some tools (paragraph) could have async hydration\n        cy.wait(100).then(() => {\n          editor.save().then((output) => {\n            const text = output.blocks[0].data.text;\n\n            expect(text).to.be.eq(newBlockData.text);\n          });\n        });\n      });\n    });\n\n    it('should update tune data when it is provided', () => {\n      /**\n       * Example Tune Class\n       */\n      class ExampleTune {\n        protected data: object;\n        /**\n         *\n         * @param data\n         */\n        constructor({ data }) {\n          this.data = data;\n        }\n\n        /**\n         * Tell editor.js that this Tool is a Block Tune\n         *\n         * @returns {boolean}\n         */\n        public static get isTune(): boolean {\n          return true;\n        }\n\n        /**\n         * Create Tunes controls wrapper that will be appended to the Block Tunes panel\n         *\n         * @returns {Element}\n         */\n        public render(): Element {\n          return document.createElement('div');\n        }\n\n        /**\n         * CSS selectors used in Tune\n         */\n        public static get CSS(): object {\n          return {};\n        }\n\n        /**\n         * Returns Tune state\n         *\n         * @returns {string}\n         */\n        public save(): object | string {\n          return this.data || '';\n        }\n      }\n\n\n      cy.createEditor({\n        tools: {\n          exampleTune: ExampleTune,\n        },\n        tunes: [ 'exampleTune' ],\n        data: {\n          blocks: [\n            {\n              id: nanoid(),\n              type: 'paragraph',\n              data: {\n                text: 'First block',\n              },\n              tunes: {\n                exampleTune: 'citation',\n              },\n            },\n          ],\n        },\n      }).as('editorInstance');\n\n      // Update the tunes data of a block\n      // Check if it is updated\n      cy.get<EditorJS>('@editorInstance')\n        .then(async (editor) => {\n          await editor.blocks.update(editor.blocks.getBlockByIndex(0).id, null, {\n            exampleTune: 'test',\n          });\n          const data = await editor.save();\n\n          const actual = JSON.stringify(data.blocks[0].tunes);\n          const expected = JSON.stringify({ exampleTune: 'test' });\n\n          expect(actual).to.eq(expected);\n        });\n    });\n\n    /**\n     * When incorrect id passed, editor should not update any block\n     */\n    it('shouldn\\'t update any block if not-existed id passed', () => {\n      cy.createEditor({\n        data: editorDataMock,\n      }).then((editor) => {\n        const idToUpdate = 'wrong-id-123';\n        const newBlockData = {\n          text: 'Updated text',\n        };\n\n        editor.blocks.update(idToUpdate, newBlockData)\n          .catch(error => {\n            expect(error.message).to.be.eq(`Block with id \"${idToUpdate}\" not found`);\n          })\n          .finally(() => {\n            cy.get('[data-cy=editorjs]')\n              .get('div.ce-block')\n              .invoke('text')\n              .then(blockText => {\n                expect(blockText).to.be.eq(firstBlock.data.text);\n              });\n          });\n      });\n    });\n  });\n\n  /**\n   * api.blocks.insert(type, data, config, index, needToFocus, replace, id)\n   */\n  describe('.insert()', function () {\n    it('should preserve block id if it is passed', function () {\n      cy.createEditor({\n        data: editorDataMock,\n      }).then((editor) => {\n        const type = 'paragraph';\n        const data = { text: 'codex' };\n        const config = undefined;\n        const index = undefined;\n        const needToFocus = undefined;\n        const replace = undefined;\n        const id = 'test-id-123';\n\n        const block = editor.blocks.insert(type, data, config, index, needToFocus, replace, id);\n\n        expect(block).not.to.be.undefined;\n        expect(block.id).to.be.eq(id);\n      });\n    });\n  });\n\n  /**\n   * api.blocks.insertMany(blocks, index)\n   */\n  describe('.insertMany()', function () {\n    it('should insert several blocks to passed index', function () {\n      cy.createEditor({\n        data: {\n          blocks: [\n            {\n              type: 'paragraph',\n              data: { text: 'first block' },\n            },\n          ],\n        },\n      }).then((editor) => {\n        const index = 0;\n\n        cy.wrap(editor.blocks.insertMany([\n          {\n            type: 'paragraph',\n            data: { text: 'inserting block #1' },\n          },\n          {\n            type: 'paragraph',\n            data: { text: 'inserting block #2' },\n          },\n        ], index)); // paste to the 0 index\n\n        cy.get('[data-cy=editorjs]')\n          .find('.ce-block')\n          .each(($el, i) => {\n            switch (i) {\n              case 0:\n                cy.wrap($el).should('have.text', 'inserting block #1');\n                break;\n              case 1:\n                cy.wrap($el).should('have.text', 'inserting block #2');\n                break;\n              case 2:\n                cy.wrap($el).should('have.text', 'first block');\n                break;\n            }\n          });\n      });\n    });\n  });\n\n  describe('.convert()', function () {\n    it('should convert a Block to another type if original Tool has \"conversionConfig.export\" and target Tool has \"conversionConfig.import\". Should return BlockAPI as well.', function () {\n      /**\n       * Mock of Tool with conversionConfig\n       */\n      class ConvertableTool extends ToolMock {\n        /**\n         * Specify how to import string data to this Tool\n         */\n        public static get conversionConfig(): ConversionConfig {\n          return {\n            import: 'text',\n          };\n        }\n\n        /**\n         * Specify how to display Tool in a Toolbox\n         */\n        public static get toolbox(): ToolboxConfig {\n          return {\n            icon: '',\n            title: 'Convertable tool',\n          };\n        }\n      }\n\n      const existingBlock = {\n        id: 'test-id-123',\n        type: 'paragraph',\n        data: {\n          text: 'Some text',\n        },\n      };\n\n      cy.createEditor({\n        tools: {\n          convertableTool: {\n            class: ConvertableTool,\n          },\n        },\n        data: {\n          blocks: [\n            existingBlock,\n          ],\n        },\n      }).then(async (editor) => {\n        const { convert } = editor.blocks;\n\n        const returnValue = await convert(existingBlock.id, 'convertableTool');\n\n        // wait for block to be converted\n        cy.wait(100).then(async () => {\n          /**\n           * Check that block was converted\n           */\n          const { blocks } = await editor.save();\n\n          expect(blocks.length).to.eq(1);\n          expect(blocks[0].type).to.eq('convertableTool');\n          expect(blocks[0].data.text).to.eq(existingBlock.data.text);\n\n          /**\n           * Check that returned value is BlockAPI\n           */\n          expect(returnValue).to.containSubset({\n            name: 'convertableTool',\n            id: blocks[0].id,\n          });\n        });\n      });\n    });\n\n    it('should throw an error if nonexisting Block id passed', function () {\n      cy.createEditor({})\n        .then((editor) => {\n          /**\n           * Call the 'convert' api method with nonexisting Block id\n           */\n          const fakeId = 'WRNG_ID';\n          const { convert } = editor.blocks;\n\n          return convert(fakeId, 'convertableTool')\n            .catch((error) => {\n              expect(error.message).to.be.eq(`Block with id \"${fakeId}\" not found`);\n            });\n        });\n    });\n\n    it('should throw an error if nonexisting Tool name passed', function () {\n      const existingBlock = {\n        id: 'test-id-123',\n        type: 'paragraph',\n        data: {\n          text: 'Some text',\n        },\n      };\n\n      cy.createEditor({\n        data: {\n          blocks: [\n            existingBlock,\n          ],\n        },\n      }).then((editor) => {\n        /**\n         * Call the 'convert' api method with nonexisting tool name\n         */\n        const nonexistingToolName = 'WRNG_TOOL_NAME';\n        const { convert } = editor.blocks;\n\n        return convert(existingBlock.id, nonexistingToolName)\n          .catch((error) => {\n            expect(error.message).to.be.eq(`Block Tool with type \"${nonexistingToolName}\" not found`);\n          });\n      });\n    });\n\n    it('should throw an error if some tool does not provide \"conversionConfig\"', function () {\n      const existingBlock = {\n        id: 'test-id-123',\n        type: 'paragraph',\n        data: {\n          text: 'Some text',\n        },\n      };\n\n      /**\n       * Mock of Tool without conversionConfig\n       */\n      class ToolWithoutConversionConfig extends ToolMock {}\n\n      cy.createEditor({\n        tools: {\n          nonConvertableTool: {\n            class: ToolWithoutConversionConfig,\n            shortcut: 'CMD+SHIFT+H',\n          },\n        },\n        data: {\n          blocks: [\n            existingBlock,\n          ],\n        },\n      }).then((editor) => {\n        /**\n         * Call the 'convert' api method with tool that does not provide \"conversionConfig\"\n         */\n        const { convert } = editor.blocks;\n\n        return convert(existingBlock.id, 'nonConvertableTool')\n          .catch((error) => {\n            expect(error.message).to.be.eq(`Conversion from \"paragraph\" to \"nonConvertableTool\" is not possible. NonConvertableTool tool(s) should provide a \"conversionConfig\"`);\n          });\n      });\n    });\n\n    it('should pass tool config to the conversionConfig.import method of the tool', function () {\n      const existingBlock = {\n        id: 'test-id-123',\n        type: 'paragraph',\n        data: {\n          text: 'Some text',\n        },\n      };\n\n      const conversionTargetToolConfig = {\n        defaultStyle: 'defaultStyle',\n      };\n\n      /**\n       * Mock of Tool with conversionConfig\n       */\n      class ToolWithConversionConfig extends ToolMock {\n        /**\n         * Specify conversion config of the tool\n         */\n        public static get conversionConfig(): {\n          /**\n           * Method that is responsible for conversion from data to string\n           */\n          export: (data: string) => string;\n\n          /**\n           * Method that is responsible for conversion from string to data\n           * Should return stringified config to see, if Editor actually passed tool config to it\n           */\n          import: (content: string, config: ToolConfig) => MockToolData;\n          } {\n          return {\n            export: (data) => data,\n            /**\n             * Passed config should be returned\n             */\n            import: (_content, config) => {\n              return { text: JSON.stringify(config) };\n            },\n          };\n        }\n      }\n\n      cy.createEditor({\n        tools: {\n          conversionTargetTool: {\n            class: ToolWithConversionConfig,\n            config: conversionTargetToolConfig,\n          },\n        },\n        data: {\n          blocks: [\n            existingBlock,\n          ],\n        },\n      }).then(async (editor) => {\n        const { convert } = editor.blocks;\n\n        await convert(existingBlock.id, 'conversionTargetTool');\n\n        // wait for block to be converted\n        cy.wait(100).then(async () => {\n          /**\n           * Check that block was converted\n           */\n          const { blocks } = await editor.save();\n\n          expect(blocks.length).to.eq(1);\n          expect(blocks[0].type).to.eq('conversionTargetTool');\n\n          /**\n           * Check that tool converted returned config as a result of import\n           */\n          expect(blocks[0].data.text).to.eq(JSON.stringify(conversionTargetToolConfig));\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/api/caret.cy.ts",
    "content": "import { createParagraphMock } from '../../support/utils/createParagraphMock';\nimport type EditorJS from '../../../../types';\n\n/**\n * Test cases for Caret API\n */\ndescribe('Caret API', () => {\n  describe('.setToBlock()', () => {\n    describe('first argument', () => {\n      const paragraphDataMock = createParagraphMock('The first block content mock.');\n\n      /**\n       * The arrange part of the following tests are the same:\n       *  - create an editor\n       *  - move caret out of the block by default\n       */\n      beforeEach(() => {\n        cy.createEditor({\n          data: {\n            blocks: [\n              paragraphDataMock,\n            ],\n          },\n        }).as('editorInstance');\n\n        /**\n         * Blur caret from the block before setting via api\n         */\n        cy.get('[data-cy=editorjs]')\n          .click();\n      });\n      it('should set caret to a block (and return true) if block index is passed as argument', () => {\n        cy.get<EditorJS>('@editorInstance')\n          .then(async (editor) => {\n            const returnedValue = editor.caret.setToBlock(0);\n\n            /**\n             * Check that caret belongs block\n             */\n            cy.window()\n              .then((window) => {\n                const selection = window.getSelection();\n                const range = selection.getRangeAt(0);\n\n                cy.get('[data-cy=editorjs]')\n                  .find('.ce-block')\n                  .first()\n                  .should(($block) => {\n                    expect($block[0].contains(range.startContainer)).to.be.true;\n                  });\n              });\n\n            expect(returnedValue).to.be.true;\n          });\n      });\n\n      it('should set caret to a block (and return true) if block id is passed as argument', () => {\n        cy.get<EditorJS>('@editorInstance')\n          .then(async (editor) => {\n            const returnedValue = editor.caret.setToBlock(paragraphDataMock.id);\n\n            /**\n             * Check that caret belongs block\n             */\n            cy.window()\n              .then((window) => {\n                const selection = window.getSelection();\n                const range = selection.getRangeAt(0);\n\n                cy.get('[data-cy=editorjs]')\n                  .find('.ce-block')\n                  .first()\n                  .should(($block) => {\n                    expect($block[0].contains(range.startContainer)).to.be.true;\n                  });\n              });\n\n            expect(returnedValue).to.be.true;\n          });\n      });\n\n      it('should set caret to a block (and return true) if Block API is passed as argument', () => {\n        cy.get<EditorJS>('@editorInstance')\n          .then(async (editor) => {\n            const block = editor.blocks.getById(paragraphDataMock.id);\n            const returnedValue = editor.caret.setToBlock(block);\n\n            /**\n             * Check that caret belongs block\n             */\n            cy.window()\n              .then((window) => {\n                const selection = window.getSelection();\n                const range = selection.getRangeAt(0);\n\n                cy.get('[data-cy=editorjs]')\n                  .find('.ce-block')\n                  .first()\n                  .should(($block) => {\n                    expect($block[0].contains(range.startContainer)).to.be.true;\n                  });\n              });\n\n            expect(returnedValue).to.be.true;\n          });\n      });\n    });\n\n    describe('offset', () => {\n      it('should set caret at specific offset in text content', () => {\n        const paragraphDataMock = createParagraphMock('Plain text content.');\n\n        cy.createEditor({\n          data: {\n            blocks: [\n              paragraphDataMock,\n            ],\n          },\n        }).as('editorInstance');\n\n        cy.get<EditorJS>('@editorInstance')\n          .then(async (editor) => {\n            const block = editor.blocks.getById(paragraphDataMock.id);\n\n            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n            editor.caret.setToBlock(block!, 'default', 5);\n\n            cy.window()\n              .then((window) => {\n                const selection = window.getSelection();\n\n                if (!selection) {\n                  throw new Error('Selection not found');\n                }\n                const range = selection.getRangeAt(0);\n\n                expect(range.startOffset).to.equal(5);\n              });\n          });\n      });\n\n      it('should set caret at correct offset when text contains HTML elements', () => {\n        const paragraphDataMock = createParagraphMock('1234<b>567</b>!');\n\n        cy.createEditor({\n          data: {\n            blocks: [\n              paragraphDataMock,\n            ],\n          },\n        }).as('editorInstance');\n\n        cy.get<EditorJS>('@editorInstance')\n          .then(async (editor) => {\n            const block = editor.blocks.getById(paragraphDataMock.id);\n\n            // Set caret after \"12345\"\n            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n            editor.caret.setToBlock(block!, 'default', 6);\n\n            cy.window()\n              .then((window) => {\n                const selection = window.getSelection();\n\n                if (!selection) {\n                  throw new Error('Selection not found');\n                }\n                const range = selection.getRangeAt(0);\n\n                expect(range.startContainer.textContent).to.equal('567');\n                expect(range.startOffset).to.equal(2);\n              });\n          });\n      });\n\n      it('should handle offset beyond content length', () => {\n        const paragraphDataMock = createParagraphMock('1234567890');\n\n        cy.createEditor({\n          data: {\n            blocks: [\n              paragraphDataMock,\n            ],\n          },\n        }).as('editorInstance');\n\n        cy.get<EditorJS>('@editorInstance')\n          .then(async (editor) => {\n            const block = editor.blocks.getById(paragraphDataMock.id);\n\n            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n            const contentLength = block!.holder.textContent?.length ?? 0;\n\n            // Set caret beyond content length\n            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n            editor.caret.setToBlock(block!, 'default', contentLength + 10);\n\n            cy.window()\n              .then((window) => {\n                const selection = window.getSelection();\n\n                if (!selection) {\n                  throw new Error('Selection not found');\n                }\n                const range = selection.getRangeAt(0);\n\n                // Should be at the end of content\n                expect(range.startOffset).to.equal(contentLength);\n              });\n          });\n      });\n\n      it('should handle offset in nested HTML structure', () => {\n        const paragraphDataMock = createParagraphMock('123<b>456<i>789</i></b>!');\n\n        cy.createEditor({\n          data: {\n            blocks: [\n              paragraphDataMock,\n            ],\n          },\n        }).as('editorInstance');\n\n        cy.get<EditorJS>('@editorInstance')\n          .then(async (editor) => {\n            const block = editor.blocks.getById(paragraphDataMock.id);\n\n\n            // Set caret after \"8\"\n            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n            editor.caret.setToBlock(block!, 'default', 8);\n\n            cy.window()\n              .then((window) => {\n                const selection = window.getSelection();\n\n                if (!selection) {\n                  throw new Error('Selection not found');\n                }\n                const range = selection.getRangeAt(0);\n\n                expect(range.startContainer.textContent).to.equal('789');\n                expect(range.startOffset).to.equal(2);\n              });\n          });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/api/toolbar.cy.ts",
    "content": "/**\n * There will be described test cases of 'api.toolbar.*' API\n */\nimport type EditorJS from '../../../../types';\n\ndescribe('api.toolbar', () => {\n  /**\n   * api.toolbar.openToolbox(openingState?: boolean)\n   */\n  const firstBlock = {\n    id: 'bwnFX5LoX7',\n    type: 'paragraph',\n    data: {\n      text: 'The first block content mock.',\n    },\n  };\n  const editorDataMock = {\n    blocks: [\n      firstBlock,\n    ],\n  };\n\n  beforeEach(function () {\n    cy.createEditor({\n      data: editorDataMock,\n      readOnly: false,\n    }).as('editorInstance');\n  });\n\n  afterEach(function () {\n    if (this.editorInstance) {\n      this.editorInstance.destroy();\n    }\n  });\n\n  describe('*.toggleToolbox()', () => {\n    const isToolboxVisible = (): void => {\n      cy.get('[data-cy=editorjs]').find('div.ce-toolbox')\n        .then((toolbox) => {\n          if (toolbox.is(':visible')) {\n            assert.isOk(true, 'Toolbox visible');\n          } else {\n            assert.isNotOk(false, 'Toolbox should be visible');\n          }\n        });\n    };\n\n    const isToolboxNotVisible = (): void => {\n      cy.get('[data-cy=editorjs]').find('div.ce-toolbox')\n        .then((toolbox) => {\n          if (!toolbox.is(':visible')) {\n            assert.isOk(true, 'Toolbox not visible');\n          } else {\n            assert.isNotOk(false, 'Toolbox should not be visible');\n          }\n        });\n    };\n\n    it('should open the toolbox', function () {\n      cy.get<EditorJS>('@editorInstance').then(async function (editor) {\n        editor.toolbar.toggleToolbox(true);\n        isToolboxVisible();\n      });\n    });\n\n    it('should close the toolbox', function () {\n      cy.get<EditorJS>('@editorInstance').then(async function (editor) {\n        editor.toolbar.toggleToolbox(true);\n\n        isToolboxVisible();\n\n        editor.toolbar.toggleToolbox(false);\n        isToolboxNotVisible();\n      });\n    });\n    it('should toggle the toolbox', function () {\n      cy.get<EditorJS>('@editorInstance').then(async function (editor) {\n        editor.toolbar.toggleToolbox();\n        isToolboxVisible();\n\n        editor.toolbar.toggleToolbox();\n        isToolboxNotVisible();\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/api/tools.cy.ts",
    "content": "import type { ToolboxConfig, BlockToolData, ToolboxConfigEntry, PasteConfig } from '../../../../types';\nimport type EditorJS from '../../../../types';\nimport type { HTMLPasteEvent, TunesMenuConfig } from '../../../../types/tools';\n\n/* eslint-disable @typescript-eslint/no-empty-function */\n\nconst ICON = '<svg width=\"17\" height=\"15\" viewBox=\"0 0 336 276\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M291 150V79c0-19-15-34-34-34H79c-19 0-34 15-34 34v42l67-44 81 72 56-29 42 30zm0 52l-43-30-56 30-81-67-66 39v23c0 19 15 34 34 34h178c17 0 31-13 34-29zM79 0h178c44 0 79 35 79 79v118c0 44-35 79-79 79H79c-44 0-79-35-79-79V79C0 35 35 0 79 0z\"></path></svg>';\n\ndescribe('Editor Tools Api', () => {\n  context('Toolbox', () => {\n    it('should render a toolbox entry for tool if configured', () => {\n      /**\n       * Tool with single toolbox entry configured\n       */\n      class TestTool {\n        /**\n         * Returns toolbox config as list of entries\n         */\n        public static get toolbox(): ToolboxConfigEntry {\n          return {\n            title: 'Entry 1',\n            icon: ICON,\n          };\n        }\n      }\n\n      cy.createEditor({\n        tools: {\n          testTool: TestTool,\n        },\n      }).as('editorInstance');\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-block')\n        .click();\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-toolbar__plus')\n        .click();\n\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-popover-item[data-item-name=testTool]')\n        .should('have.length', 1);\n\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-popover-item[data-item-name=testTool] .ce-popover-item__icon')\n        .should('contain.html', TestTool.toolbox.icon);\n    });\n\n    it('should render several toolbox entries for one tool if configured', () => {\n      /**\n       * Tool with several toolbox entries configured\n       */\n      class TestTool {\n        /**\n         * Returns toolbox config as list of entries\n         */\n        public static get toolbox(): ToolboxConfig {\n          return [\n            {\n              title: 'Entry 1',\n              icon: ICON,\n            },\n            {\n              title: 'Entry 2',\n              icon: ICON,\n            },\n          ];\n        }\n      }\n\n      cy.createEditor({\n        tools: {\n          testTool: TestTool,\n        },\n      }).as('editorInstance');\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-block')\n        .click();\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-toolbar__plus')\n        .click();\n\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-popover-item[data-item-name=testTool]')\n        .should('have.length', 2);\n\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-popover-item[data-item-name=testTool]')\n        .first()\n        .should('contain.text', TestTool.toolbox[0].title);\n\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-popover-item[data-item-name=testTool]')\n        .last()\n        .should('contain.text', TestTool.toolbox[1].title);\n    });\n\n    it('should insert block with overridden data on entry click in case toolbox entry provides data overrides', () => {\n      const text = 'Text';\n      const dataOverrides = {\n        testProp: 'new value',\n      };\n\n      /**\n       * Tool with default data to be overridden\n       */\n      class TestTool {\n        private _data = {\n          testProp: 'default value',\n        };\n\n        /**\n         * Tool constructor\n         *\n         * @param data - previously saved data\n         */\n        constructor({ data }) {\n          this._data = data;\n        }\n\n        /**\n         * Returns toolbox config as list of entries with overridden data\n         */\n        public static get toolbox(): ToolboxConfig {\n          return [\n            {\n              title: 'Entry 1',\n              icon: ICON,\n              data: dataOverrides,\n            },\n          ];\n        }\n\n        /**\n         * Return Tool's view\n         */\n        public render(): HTMLElement {\n          const wrapper = document.createElement('div');\n\n          wrapper.setAttribute('contenteditable', 'true');\n\n          return wrapper;\n        }\n\n        /**\n         * Extracts Tool's data from the view\n         *\n         * @param el - tool view\n         */\n        public save(el: HTMLElement): BlockToolData {\n          return {\n            ...this._data,\n            text: el.innerHTML,\n          };\n        }\n      }\n\n      cy.createEditor({\n        tools: {\n          testTool: TestTool,\n        },\n      }).as('editorInstance');\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-block')\n        .click();\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-toolbar__plus')\n        .click();\n\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-popover-item[data-item-name=testTool]')\n        .click();\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-block')\n        .last()\n        .click()\n        .type(text);\n\n      cy.get('@editorInstance')\n        .then(async (editor: unknown) => {\n          const editorData = await (editor as EditorJS).save();\n\n          expect(editorData.blocks[0].data).to.be.deep.eq({\n            ...dataOverrides,\n            text,\n          });\n        });\n    });\n  });\n\n  context('Tunes — renderSettings()', () => {\n    it('should contain a single block tune configured in tool\\'s renderSettings() method', () => {\n      /** Tool with single tunes menu entry configured */\n      class TestTool {\n        /** Returns toolbox config as list of entries */\n        public static get toolbox(): ToolboxConfigEntry {\n          return {\n            title: 'Test tool',\n            icon: ICON,\n          };\n        }\n\n        /** Returns configuration for block tunes menu */\n        public renderSettings(): TunesMenuConfig {\n          return {\n            label: 'Test tool tune',\n            icon: ICON,\n            name: 'testToolTune',\n\n            onActivate: (): void => { },\n          };\n        }\n\n        /** Save method stub */\n        public save(): void { }\n\n        /** Renders a block */\n        public render(): HTMLElement {\n          const element = document.createElement('div');\n\n          element.contentEditable = 'true';\n          element.setAttribute('data-name', 'testBlock');\n\n          return element;\n        }\n      }\n\n      cy.createEditor({\n        tools: {\n          testTool: TestTool,\n        },\n      }).as('editorInstance');\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-block')\n        .click();\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-toolbar__plus')\n        .click();\n\n      // Insert test tool block\n      cy.get('[data-cy=editorjs]')\n        .get(`[data-item-name=\"testTool\"]`)\n        .click();\n\n      cy.get('[data-cy=editorjs]')\n        .get('[data-name=testBlock]')\n        .type('some text')\n        .click();\n\n      // Open block tunes\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-toolbar__settings-btn')\n        .click();\n\n      // Expect preconfigured tune to exist in tunes menu\n      cy.get('[data-item-name=testToolTune]').should('exist');\n    });\n\n    it('should contain multiple block tunes if configured in tool\\'s renderSettings() method', () => {\n      /** Tool with single tunes menu entry configured */\n      class TestTool {\n        /** Returns toolbox config as list of entries */\n        public static get toolbox(): ToolboxConfigEntry {\n          return {\n            title: 'Test tool',\n            icon: ICON,\n          };\n        }\n\n        /** Returns configuration for block tunes menu */\n        public renderSettings(): TunesMenuConfig {\n          return [\n            {\n              label: 'Test tool tune 1',\n              icon: ICON,\n              name: 'testToolTune1',\n\n              onActivate: (): void => { },\n            },\n            {\n              label: 'Test tool tune 2',\n              icon: ICON,\n              name: 'testToolTune2',\n\n              onActivate: (): void => { },\n            },\n          ];\n        }\n\n        /** Save method stub */\n        public save(): void { }\n\n        /** Renders a block */\n        public render(): HTMLElement {\n          const element = document.createElement('div');\n\n          element.contentEditable = 'true';\n          element.setAttribute('data-name', 'testBlock');\n\n          return element;\n        }\n      }\n\n      cy.createEditor({\n        tools: {\n          testTool: TestTool,\n        },\n      }).as('editorInstance');\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-block')\n        .click();\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-toolbar__plus')\n        .click();\n\n      // Insert test tool block\n      cy.get('[data-cy=editorjs]')\n        .get(`[data-item-name=\"testTool\"]`)\n        .click();\n\n      cy.get('[data-cy=editorjs]')\n        .get('[data-name=testBlock]')\n        .type('some text')\n        .click();\n\n      // Open block tunes\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-toolbar__settings-btn')\n        .click();\n\n      // Expect preconfigured tunes to exist in tunes menu\n      cy.get('[data-item-name=testToolTune1]').should('exist');\n      cy.get('[data-item-name=testToolTune2]').should('exist');\n    });\n\n    it('should contain block tunes represented as custom html if so configured in tool\\'s renderSettings() method', () => {\n      const sampleText = 'sample text';\n\n      /** Tool with single tunes menu entry configured */\n      class TestTool {\n        /** Returns toolbox config as list of entries */\n        public static get toolbox(): ToolboxConfigEntry {\n          return {\n            title: 'Test tool',\n            icon: ICON,\n          };\n        }\n\n        /** Returns configuration for block tunes menu */\n        public renderSettings(): HTMLElement {\n          const element = document.createElement('div');\n\n          element.textContent = sampleText;\n\n          return element;\n        }\n\n        /** Save method stub */\n        public save(): void { }\n\n        /** Renders a block */\n        public render(): HTMLElement {\n          const element = document.createElement('div');\n\n          element.contentEditable = 'true';\n          element.setAttribute('data-name', 'testBlock');\n\n          return element;\n        }\n      }\n\n      cy.createEditor({\n        tools: {\n          testTool: TestTool,\n        },\n      }).as('editorInstance');\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-block')\n        .click();\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-toolbar__plus')\n        .click();\n\n      // Insert test tool block\n      cy.get('[data-cy=editorjs]')\n        .get(`[data-item-name=\"testTool\"]`)\n        .click();\n\n      cy.get('[data-cy=editorjs]')\n        .get('[data-name=testBlock]')\n        .type('some text')\n        .click();\n\n      // Open block tunes\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-toolbar__settings-btn')\n        .click();\n\n      // Expect preconfigured custom html tunes to exist in tunes menu\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-popover')\n        .should('contain.text', sampleText);\n    });\n\n    it('should support label alias', () => {\n      /** Tool with single tunes menu entry configured */\n      class TestTool {\n        /** Returns toolbox config as list of entries */\n        public static get toolbox(): ToolboxConfigEntry {\n          return {\n            title: 'Test tool',\n            icon: ICON,\n          };\n        }\n\n        /** Returns configuration for block tunes menu */\n        public renderSettings(): TunesMenuConfig {\n          return [\n            {\n              icon: ICON,\n              name: 'testToolTune1',\n              onActivate: (): void => {},\n\n              // Set text via title property\n              title: 'Test tool tune 1',\n            },\n            {\n              icon: ICON,\n              name: 'testToolTune2',\n              onActivate: (): void => {},\n\n              // Set test via label property\n              label: 'Test tool tune 2',\n            },\n          ];\n        }\n\n        /** Save method stub */\n        public save(): void {}\n\n        /** Renders a block */\n        public render(): HTMLElement {\n          const element = document.createElement('div');\n\n          element.contentEditable = 'true';\n          element.setAttribute('data-name', 'testBlock');\n\n          return element;\n        }\n      }\n\n      cy.createEditor({\n        tools: {\n          testTool: TestTool,\n        },\n      }).as('editorInstance');\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-block')\n        .click();\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-toolbar__plus')\n        .click();\n\n      // Insert test tool block\n      cy.get('[data-cy=editorjs]')\n        .get(`[data-item-name=\"testTool\"]`)\n        .click();\n\n      cy.get('[data-cy=editorjs]')\n        .get('[data-name=testBlock]')\n        .type('some text')\n        .click();\n\n      // Open block tunes\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-toolbar__settings-btn')\n        .click();\n\n      // Expect both tunes to have correct text\n      cy.get('[data-item-name=testToolTune1]').contains('Test tool tune 1');\n      cy.get('[data-item-name=testToolTune2]').contains('Test tool tune 2');\n    });\n  });\n\n  /**\n   * @todo cover all the pasteConfig properties\n   */\n  context('Paste — pasteConfig()', () => {\n    context('tags', () => {\n      /**\n       * tags: ['H1', 'H2']\n       */\n      it('should use corresponding tool when the array of tag names specified', () => {\n        /**\n         * Test tool with pasteConfig.tags specified\n         */\n        class TestImgTool {\n          /** config specified handled tag */\n          public static get pasteConfig(): PasteConfig {\n            return {\n              tags: [ 'img' ], // only tag name specified. Attributes should be sanitized\n            };\n          }\n\n          /** onPaste callback will be stubbed below */\n          public onPaste(): void { }\n\n          /** save is required for correct implementation of the BlockTool class */\n          public save(): void { }\n\n          /** render is required for correct implementation of the BlockTool class */\n          public render(): HTMLElement {\n            return document.createElement('img');\n          }\n        }\n\n        const toolsOnPaste = cy.spy(TestImgTool.prototype, 'onPaste');\n\n        cy.createEditor({\n          tools: {\n            testTool: TestImgTool,\n          },\n        }).as('editorInstance');\n\n        cy.get('[data-cy=editorjs]')\n          .get('div.ce-block')\n          .click()\n          .paste({\n            // eslint-disable-next-line @typescript-eslint/naming-convention\n            'text/html': '<img>',\n          })\n          .then(() => {\n            expect(toolsOnPaste).to.be.called;\n          });\n      });\n\n      /**\n       * tags: ['img'] -> <img>\n       */\n      it('should sanitize all attributes from tag, if only tag name specified ', () => {\n        /**\n         * Variable used for spying the pasted element we are passing to the Tool\n         */\n        let pastedElement;\n\n        /**\n         * Test tool with pasteConfig.tags specified\n         */\n        class TestImageTool {\n          /** config specified handled tag */\n          public static get pasteConfig(): PasteConfig {\n            return {\n              tags: [ 'img' ], // only tag name specified. Attributes should be sanitized\n            };\n          }\n\n          /** onPaste callback will be stubbed below */\n          public onPaste(): void { }\n\n          /** save is required for correct implementation of the BlockTool class */\n          public save(): void { }\n\n          /** render is required for correct implementation of the BlockTool class */\n          public render(): HTMLElement {\n            return document.createElement('img');\n          }\n        }\n\n        /**\n         * Stub the onPaste method to access the PasteEvent data for assertion\n         */\n        cy.stub(TestImageTool.prototype, 'onPaste').callsFake((event: HTMLPasteEvent) => {\n          pastedElement = event.detail.data;\n        });\n\n        cy.createEditor({\n          tools: {\n            testImageTool: TestImageTool,\n          },\n        });\n\n        cy.get('[data-cy=editorjs]')\n          .get('div.ce-block')\n          .click()\n          .paste({\n            // eslint-disable-next-line @typescript-eslint/naming-convention\n            'text/html': '<img src=\"foo\" onerror=\"alert(123)\"/>', // all attributes should be sanitized\n          })\n          .then(() => {\n            expect(pastedElement).not.to.be.undefined;\n            expect(pastedElement.tagName.toLowerCase()).eq('img');\n            expect(pastedElement.attributes.length).eq(0);\n          });\n      });\n\n      /**\n       * tags: ['OL','LI',]\n       * -><ol>\n       * <li></li>\n       * <li></li>\n       * </ol>\n       */\n      it('should sanitize all attributes from tags, even if tag names specified in uppercase', () => {\n        /**\n         * Variable used for spying the pasted element we are passing to the Tool\n         */\n        let pastedElement;\n\n        /**\n         * Test tool with pasteConfig.tags specified\n         */\n        class TestListTool {\n          /** config specified handled tag */\n          public static get pasteConfig(): PasteConfig {\n            return {\n              tags: ['OL', 'LI'], // tag names specified in upper case\n            };\n          }\n\n          /** onPaste callback will be stubbed below */\n          public onPaste(): void { }\n\n          /** save is required for correct implementation of the BlockTool class */\n          public save(): void { }\n\n          /** render is required for correct implementation of the BlockTool class */\n          public render(): HTMLElement {\n            return document.createElement('ol');\n          }\n        }\n\n        /**\n         * Stub the onPaste method to access the PasteEvent data for assertion\n         */\n        cy.stub(TestListTool.prototype, 'onPaste').callsFake((event: HTMLPasteEvent) => {\n          pastedElement = event.detail.data;\n        });\n\n        cy.createEditor({\n          tools: {\n            testListTool: TestListTool,\n          },\n        });\n\n        cy.get('[data-cy=editorjs]')\n          .get('div.ce-block')\n          .click()\n          .paste({\n            // eslint-disable-next-line @typescript-eslint/naming-convention\n            'text/html': '<ol start=\"50\"><li>Orderd List</li><li>Unorderd List</li></ol>', // all attributes should be sanitized, <li> should be preserved\n          })\n          .then(() => {\n            expect(pastedElement).not.to.be.undefined;\n            expect(pastedElement.tagName.toLowerCase()).eq('ol');\n            expect(pastedElement.attributes.length).eq(0);\n            // check number of children\n            expect(pastedElement.children.length).eq(2);\n\n            /**\n             * Check that all children are <li> tags\n             */\n            pastedElement.childNodes.forEach((child) => {\n              expect(child.tagName.toLowerCase()).eq('li');\n              expect(child.attributes.length).eq(0);\n            });\n          });\n      });\n\n      /**\n       * tags: [{\n       *   img: {\n       *     src: true\n       *   }\n       * }]\n       *   ->  <img src=\"\">\n       *\n       */\n      it('should leave attributes if entry specified as a sanitizer config ', () => {\n        /**\n         * Variable used for spying the pasted element we are passing to the Tool\n         */\n        let pastedElement;\n\n        /**\n         * Test tool with pasteConfig.tags specified\n         */\n        class TestImageTool {\n          /** config specified handled tag */\n          public static get pasteConfig(): PasteConfig {\n            return {\n              tags: [\n                {\n                  img: {\n                    src: true,\n                  },\n                },\n              ],\n            };\n          }\n\n          /** onPaste callback will be stubbed below */\n          public onPaste(): void { }\n\n          /** save is required for correct implementation of the BlockTool class */\n          public save(): void { }\n\n          /** render is required for correct implementation of the BlockTool class */\n          public render(): HTMLElement {\n            return document.createElement('img');\n          }\n        }\n\n        /**\n         * Stub the onPaste method to access the PasteEvent data for assertion\n         */\n        cy.stub(TestImageTool.prototype, 'onPaste').callsFake((event: HTMLPasteEvent) => {\n          pastedElement = event.detail.data;\n        });\n\n        cy.createEditor({\n          tools: {\n            testImageTool: TestImageTool,\n          },\n        });\n\n        cy.get('[data-cy=editorjs]')\n          .get('div.ce-block')\n          .click()\n          .paste({\n            // eslint-disable-next-line @typescript-eslint/naming-convention\n            'text/html': '<img src=\"foo\" onerror=\"alert(123)\"/>',\n          })\n          .then(() => {\n            expect(pastedElement).not.to.be.undefined;\n\n            /**\n             * Check that the <img> has only \"src\" attribute\n             */\n            expect(pastedElement.tagName.toLowerCase()).eq('img');\n            expect(pastedElement.getAttribute('src')).eq('foo');\n            expect(pastedElement.attributes.length).eq(1);\n          });\n      });\n\n      /**\n       * tags: [\n       *   'video',\n       *   {\n       *     source: {\n       *       src: true\n       *     }\n       *   }\n       * ]\n       */\n      it('should support mixed tag names and sanitizer config ', () => {\n        /**\n         * Variable used for spying the pasted element we are passing to the Tool\n         */\n        let pastedElement;\n\n        /**\n         * Test tool with pasteConfig.tags specified\n         */\n        class TestTool {\n          /** config specified handled tag */\n          public static get pasteConfig(): PasteConfig {\n            return {\n              tags: [\n                'video', // video should not have attributes\n                {\n                  source: { // source should have only src attribute\n                    src: true,\n                  },\n                },\n              ],\n            };\n          }\n\n          /** onPaste callback will be stubbed below */\n          public onPaste(): void { }\n\n          /** save is required for correct implementation of the BlockTool class */\n          public save(): void { }\n\n          /** render is required for correct implementation of the BlockTool class */\n          public render(): HTMLElement {\n            return document.createElement('video');\n          }\n        }\n\n        /**\n         * Stub the onPaste method to access the PasteEvent data for assertion\n         */\n        cy.stub(TestTool.prototype, 'onPaste').callsFake((event: HTMLPasteEvent) => {\n          pastedElement = event.detail.data;\n        });\n\n        cy.createEditor({\n          tools: {\n            testTool: TestTool,\n          },\n        });\n\n        cy.get('[data-cy=editorjs]')\n          .get('div.ce-block')\n          .click()\n          .paste({\n            // eslint-disable-next-line @typescript-eslint/naming-convention\n            'text/html': '<video width=\"100\"><source src=\"movie.mp4\" type=\"video/mp4\"></video>',\n          })\n          .then(() => {\n            expect(pastedElement).not.to.be.undefined;\n\n            /**\n             * Check that <video>  has no attributes\n             */\n            expect(pastedElement.tagName.toLowerCase()).eq('video');\n            expect(pastedElement.attributes.length).eq(0);\n\n            /**\n             * Check that the <source> has only 'src' attribute\n             */\n            expect(pastedElement.firstChild.tagName.toLowerCase()).eq('source');\n            expect(pastedElement.firstChild.getAttribute('src')).eq('movie.mp4');\n            expect(pastedElement.firstChild.attributes.length).eq(1);\n          });\n      });\n\n      /**\n       * tags: [\n       *   {\n       *     td: { width: true },\n       *     tr: { height: true }\n       *   }\n       * ]\n       */\n      it('should support config with several keys as the single entry', () => {\n        /**\n         * Variable used for spying the pasted element we are passing to the Tool\n         */\n        let pastedElement;\n\n        /**\n         * Test tool with pasteConfig.tags specified\n         */\n        class TestTool {\n          /** config specified handled tag */\n          public static get pasteConfig(): PasteConfig {\n            return {\n              tags: [\n                {\n                  video: {\n                    width: true,\n                  },\n                  source: {\n                    src: true,\n                  },\n                },\n              ],\n            };\n          }\n\n          /** onPaste callback will be stubbed below */\n          public onPaste(): void { }\n\n          /** save is required for correct implementation of the BlockTool class */\n          public save(): void { }\n\n          /** render is required for correct implementation of the BlockTool class */\n          public render(): HTMLElement {\n            return document.createElement('video');\n          }\n        }\n\n        /**\n         * Stub the onPaste method to access the PasteEvent data for assertion\n         */\n        cy.stub(TestTool.prototype, 'onPaste').callsFake((event: HTMLPasteEvent) => {\n          pastedElement = event.detail.data;\n        });\n\n        cy.createEditor({\n          tools: {\n            testTool: TestTool,\n          },\n        });\n\n        cy.get('[data-cy=editorjs]')\n          .get('div.ce-block')\n          .click()\n          .paste({\n            // eslint-disable-next-line @typescript-eslint/naming-convention\n            'text/html': '<video width=\"100\"><source src=\"movie.mp4\" type=\"video/mp4\"></video>',\n          })\n          .then(() => {\n            expect(pastedElement).not.to.be.undefined;\n            expect(pastedElement.tagName.toLowerCase()).eq('video');\n\n            /**\n             * Check that the <tr> has the 'height' attribute\n             */\n            expect(pastedElement.firstChild.tagName.toLowerCase()).eq('source');\n            expect(pastedElement.firstChild.getAttribute('src')).eq('movie.mp4');\n          });\n      });\n\n      /**\n       * It covers a workaround HTMLJanitor bug with tables (incorrect sanitizing of table.innerHTML)\n       * https://github.com/guardian/html-janitor/issues/3\n       */\n      it('should correctly sanitize Table structure (test for HTMLJanitor bug)', () => {\n        /**\n         * Variable used for spying the pasted element we are passing to the Tool\n         */\n        let pastedElement;\n\n        /**\n         * Test tool with pasteConfig.tags specified\n         */\n        class TestTool {\n          /** config specified handled tag */\n          public static get pasteConfig(): PasteConfig {\n            return {\n              tags: [\n                'table',\n                'tbody',\n                {\n                  td: {\n                    width: true,\n                  },\n                  tr: {\n                    height: true,\n                  },\n                },\n              ],\n            };\n          }\n\n          /** onPaste callback will be stubbed below */\n          public onPaste(): void { }\n\n          /** save is required for correct implementation of the BlockTool class */\n          public save(): void { }\n\n          /** render is required for correct implementation of the BlockTool class */\n          public render(): HTMLElement {\n            return document.createElement('tbody');\n          }\n        }\n\n        /**\n         * Stub the onPaste method to access the PasteEvent data for assertion\n         */\n        cy.stub(TestTool.prototype, 'onPaste').callsFake((event: HTMLPasteEvent) => {\n          pastedElement = event.detail.data;\n        });\n\n        cy.createEditor({\n          tools: {\n            testTool: TestTool,\n          },\n        });\n\n        cy.get('[data-cy=editorjs]')\n          .get('div.ce-block')\n          .click()\n          .paste({\n            // eslint-disable-next-line @typescript-eslint/naming-convention\n            'text/html': '<table><tr height=\"50\"><td width=\"300\">Ho-Ho-Ho</td></tr></table>',\n          })\n          .then(() => {\n            expect(pastedElement).not.to.be.undefined;\n            expect(pastedElement.tagName.toLowerCase()).eq('table');\n\n            /**\n             * Check that the <tr> has the 'height' attribute\n             */\n            expect(pastedElement.querySelector('tr')).not.to.be.undefined;\n            expect(pastedElement.querySelector('tr').getAttribute('height')).eq('50');\n\n            /**\n             * Check that the <td> has the 'width' attribute\n             */\n            expect(pastedElement.querySelector('td')).not.to.be.undefined;\n            expect(pastedElement.querySelector('td').getAttribute('width')).eq('300');\n          });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/api/tunes.cy.ts",
    "content": "import type { TunesMenuConfig } from '../../../../types/tools';\n\n/* eslint-disable @typescript-eslint/no-empty-function */\n\ndescribe('Editor Tunes Api', () => {\n  it('should render a popover entry for block tune if configured', () => {\n    /** Test tune that should appear be rendered in block tunes menu */\n    class TestTune {\n      /** Set Tool is Tune */\n      public static readonly isTune = true;\n\n      /** Tune's appearance in block settings menu */\n      public render(): TunesMenuConfig {\n        return {\n          icon: 'ICON',\n          title: 'Test tune',\n          name: 'testTune',\n\n          onActivate: (): void => { },\n        };\n      }\n\n      /** Save method stub */\n      public save(): void {}\n    }\n\n    cy.createEditor({\n      tools: {\n        testTune: TestTune,\n      },\n      tunes: [ 'testTune' ],\n    }).as('editorInstance');\n\n    cy.get('[data-cy=editorjs]')\n      .get('div.ce-block')\n      .type('some text')\n      .click();\n\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-toolbar__settings-btn')\n      .click();\n\n    cy.get('[data-item-name=testTune]').should('exist');\n  });\n\n  it('should render several popover entries for block tune if configured', () => {\n    /** Test tune that should appear be rendered in block tunes menu */\n    class TestTune {\n      /** Set Tool is Tune */\n      public static readonly isTune = true;\n\n      /** Tune's appearance in block settings menu */\n      public render(): TunesMenuConfig {\n        return [\n          {\n            icon: 'ICON1',\n            title: 'Tune entry 1',\n            name: 'testTune1',\n\n            onActivate: (): void => { },\n          }, {\n            icon: 'ICON2',\n            title: 'Tune entry 2',\n            name: 'testTune2',\n\n            onActivate: (): void => { },\n          },\n        ];\n      }\n\n      /** Save method stub */\n      public save(): void {}\n    }\n\n    cy.createEditor({\n      tools: {\n        testTune: TestTune,\n      },\n      tunes: [ 'testTune' ],\n    }).as('editorInstance');\n\n    cy.get('[data-cy=editorjs]')\n      .get('div.ce-block')\n      .type('some text')\n      .click();\n\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-toolbar__settings-btn')\n      .click();\n\n    cy.get('[data-item-name=testTune1]').should('exist');\n    cy.get('[data-item-name=testTune2]').should('exist');\n  });\n\n  it('should display custom html returned by tune\\'s render() method inside tunes menu', () => {\n    const sampleText = 'sample text';\n\n    /** Test tune that should appear be rendered in block tunes menu  */\n    class TestTune {\n      /** Set Tool is Tune */\n      public static readonly isTune = true;\n\n      /** Tune's appearance in block settings menu */\n      public render(): HTMLElement {\n        const element = document.createElement('div');\n\n        element.textContent = sampleText;\n\n        return element;\n      }\n\n      /** Save method stub */\n      public save(): void {}\n    }\n\n    cy.createEditor({\n      tools: {\n        testTune: TestTune,\n      },\n      tunes: [ 'testTune' ],\n    }).as('editorInstance');\n\n    cy.get('[data-cy=editorjs]')\n      .get('div.ce-block')\n      .type('some text')\n      .click();\n\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-toolbar__settings-btn')\n      .click();\n\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-popover')\n      .should('contain.text', sampleText);\n  });\n\n  it('should support label alias', () => {\n    /** Test tune that should appear be rendered in block tunes menu */\n    class TestTune {\n      /** Set Tool is Tune */\n      public static readonly isTune = true;\n\n      /** Tune's appearance in block settings menu */\n      public render(): TunesMenuConfig {\n        return [\n          {\n            icon: 'ICON1',\n            name: 'testTune1',\n            onActivate: (): void => { },\n\n            // Set text via title property\n            title: 'Tune entry 1',\n          }, {\n            icon: 'ICON2',\n            name: 'testTune2',\n            onActivate: (): void => { },\n\n            // Set text via label property\n            label: 'Tune entry 2',\n          },\n        ];\n      }\n\n      /** Save method stub */\n      public save(): void {}\n    }\n\n    cy.createEditor({\n      tools: {\n        testTune: TestTune,\n      },\n      tunes: [ 'testTune' ],\n    }).as('editorInstance');\n\n    cy.get('[data-cy=editorjs]')\n      .get('div.ce-block')\n      .type('some text')\n      .click();\n\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-toolbar__settings-btn')\n      .click();\n\n\n    /** Check both tunes have correct text */\n    cy.get('[data-item-name=testTune1]').contains('Tune entry 1');\n    cy.get('[data-item-name=testTune2]').contains('Tune entry 2');\n  });\n\n  it('should display installed tunes above default tunes', () => {\n    /** Test tune that should appear be rendered in block tunes menu */\n    class TestTune {\n      /** Set Tool is Tune */\n      public static readonly isTune = true;\n\n      /** Tune's appearance in block settings menu */\n      public render(): TunesMenuConfig {\n        return [\n          {\n            icon: 'ICON',\n            label: 'Tune entry',\n            name: 'test-tune',\n\n            onActivate: (): void => { },\n          },\n        ];\n      }\n\n      /** Save method stub */\n      public save(): void {}\n    }\n\n    cy.createEditor({\n      tools: {\n        testTune: TestTune,\n      },\n      tunes: [ 'testTune' ],\n    }).as('editorInstance');\n\n    cy.get('[data-cy=editorjs]')\n      .get('div.ce-block')\n      .type('some text')\n      .click();\n\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-toolbar__settings-btn')\n      .click();\n\n    /** Check test tune is inserted at index 0 */\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-settings .ce-popover-item')\n      .eq(0)\n      .should('have.attr', 'data-item-name', 'test-tune' );\n\n    /** Check default Move Up tune is inserted below the test tune */\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-settings .ce-popover-item')\n      .eq(1)\n      .should('have.attr', 'data-item-name', 'move-up' );\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/block-ids.cy.ts",
    "content": "import Header from '@editorjs/header';\nimport { nanoid } from 'nanoid';\nimport type EditorJS from '../../../types/index';\n\n\ndescribe('Block ids', () => {\n  it('Should generate unique block ids for new blocks', () => {\n    cy.createEditor({\n      tools: {\n        header: Header,\n      },\n    }).as('editorInstance');\n\n    cy.get('[data-cy=editorjs]')\n      .get('div.ce-block')\n      .click()\n      .type('First block ')\n      .type('{enter}')\n      .get('div.ce-block')\n      .last()\n      .type('Second block ')\n      .type('{enter}');\n\n    cy.get('[data-cy=editorjs]')\n      .get('div.ce-toolbar__plus')\n      .click();\n\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-popover-item[data-item-name=header]')\n      .click();\n\n    cy.get('[data-cy=editorjs]')\n      .get('div.ce-block')\n      .last()\n      .click()\n      .type('Header');\n\n    cy.get<EditorJS>('@editorInstance')\n      .then(async (editor) => {\n        const data = await editor.save();\n\n        data.blocks.forEach(block => {\n          expect(typeof block.id).to.eq('string');\n        });\n      });\n  });\n\n  it('should preserve passed ids', () => {\n    cy.createEditor({})\n      .as('editorInstance');\n\n    const blocks = [\n      {\n        id: nanoid(),\n        type: 'paragraph',\n        data: {\n          text: 'First block',\n        },\n      },\n      {\n        id: nanoid(),\n        type: 'paragraph',\n        data: {\n          text: 'Second block',\n        },\n      },\n    ];\n\n    cy.get<EditorJS>('@editorInstance')\n      .render({\n        blocks,\n      });\n\n    cy.get<EditorJS>('@editorInstance')\n      .then(async (editor) => {\n        const data = await editor.save();\n\n        data.blocks.forEach((block, index) => {\n          expect(block.id).to.eq(blocks[index].id);\n        });\n      });\n  });\n\n  it('should preserve passed ids if blocks were added', () => {\n    cy.createEditor({})\n      .as('editorInstance');\n\n    const blocks = [\n      {\n        id: nanoid(),\n        type: 'paragraph',\n        data: {\n          text: 'First block',\n        },\n      },\n      {\n        id: nanoid(),\n        type: 'paragraph',\n        data: {\n          text: 'Second block',\n        },\n      },\n    ];\n\n    cy.get<EditorJS>('@editorInstance')\n      .render({\n        blocks,\n      });\n\n    cy.get('[data-cy=editorjs]')\n      .get('div.ce-block')\n      .first()\n      .click()\n      .type('{enter}')\n      .next()\n      .type('Middle block');\n\n    cy.get<EditorJS>('@editorInstance')\n      .then(async (editor) => {\n        cy.wrap(await editor.save())\n          .then((data) => {\n            expect(data.blocks[0].id).to.eq(blocks[0].id);\n            expect(data.blocks[2].id).to.eq(blocks[1].id);\n          });\n      });\n  });\n\n  it('should be stored at the Block wrapper\\'s data-id attribute', () => {\n    cy.createEditor({})\n      .as('editorInstance');\n\n    const blocks = [\n      {\n        id: nanoid(),\n        type: 'paragraph',\n        data: {\n          text: 'First block',\n        },\n      },\n      {\n        id: nanoid(),\n        type: 'paragraph',\n        data: {\n          text: 'Second block',\n        },\n      },\n    ];\n\n    cy.get<EditorJS>('@editorInstance')\n      .render({\n        blocks,\n      });\n\n    cy.get('[data-cy=editorjs]')\n      .get('div.ce-block')\n      .each(($block, index) => {\n        expect($block.attr('data-id')).to.eq(blocks[index].id);\n      });\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/copy-paste.cy.ts",
    "content": "import Header from '@editorjs/header';\nimport Image from '@editorjs/simple-image';\nimport * as _ from '../../../src/components/utils';\nimport type { BlockTool, BlockToolData, OutputData } from '../../../types';\nimport $ from '../../../src/components/dom';\nimport type EditorJS from '../../../types/index';\n\n\ndescribe('Copy pasting from Editor', function () {\n  context('pasting', function () {\n    it('should paste plain text', function () {\n      cy.createEditor({});\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-block')\n        .as('block')\n        .click()\n        .paste({\n          // eslint-disable-next-line @typescript-eslint/naming-convention\n          'text/plain': 'Some plain text',\n        });\n\n      cy.get('@block').should('contain', 'Some plain text');\n    });\n\n    it('should paste inline html data', function () {\n      cy.createEditor({});\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-block')\n        .as('block')\n        .click()\n        .paste({\n          // eslint-disable-next-line @typescript-eslint/naming-convention\n          'text/html': '<p><b>Some text</b></p>',\n        });\n\n      cy.get('@block').should('contain.html', '<b>Some text</b>');\n    });\n\n    it('should paste several blocks if plain text contains new lines', function () {\n      cy.createEditor({});\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-block')\n        .click()\n        .paste({\n          // eslint-disable-next-line @typescript-eslint/naming-convention\n          'text/plain': 'First block\\n\\nSecond block',\n        });\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-block')\n        .then(blocks => {\n          expect(blocks[0].textContent).to.eq('First block');\n          expect(blocks[1].textContent).to.eq('Second block');\n        });\n    });\n\n    it('should paste several blocks if html contains several paragraphs', function () {\n      cy.createEditor({});\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-block')\n        .click()\n        .paste({\n          // eslint-disable-next-line @typescript-eslint/naming-convention\n          'text/html': '<p>First block</p><p>Second block</p>',\n        });\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-block')\n        .then(blocks => {\n          expect(blocks[0].textContent).to.eq('First block');\n          expect(blocks[1].textContent).to.eq('Second block');\n        });\n    });\n\n    it('should paste using custom data type', function () {\n      cy.createEditor({});\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-block')\n        .click()\n        .paste({\n          // eslint-disable-next-line @typescript-eslint/naming-convention\n          'application/x-editor-js': JSON.stringify([\n            {\n              tool: 'paragraph',\n              data: {\n                text: 'First block',\n              },\n            },\n            {\n              tool: 'paragraph',\n              data: {\n                text: 'Second block',\n              },\n            },\n          ]),\n        });\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-block')\n        .then(blocks => {\n          expect(blocks[0].textContent).to.eq('First block');\n          expect(blocks[1].textContent).to.eq('Second block');\n        });\n    });\n\n    it('should parse block tags', function () {\n      cy.createEditor({\n        tools: {\n          header: Header,\n        },\n      }).as('editorInstance');\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-block')\n        .click()\n        .paste({\n          // eslint-disable-next-line @typescript-eslint/naming-convention\n          'text/html': '<h2>First block</h2><p>Second block</p>',\n        });\n\n      /**\n       * Check inserted blocks\n       */\n      cy.get('[data-cy=editorjs]')\n        .get('h2.ce-header')\n        .should('contain', 'First block');\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-paragraph')\n        .should('contain', 'Second block');\n\n      /**\n       * Check saved data as well\n       */\n      cy.get<EditorJS>('@editorInstance')\n        .then(async (editor) => {\n          cy.wrap<OutputData>(await editor.save())\n            .then((data) => {\n              /**\n               * <h2> has been correctly saved\n               */\n              expect(data.blocks[0].type).to.eq('header');\n              expect(data.blocks[0].data.text).to.eq('First block');\n              expect(data.blocks[0].data.level).to.eq(2);\n\n              /**\n               * <p> has been correctly saved\n               */\n              expect(data.blocks[1].type).to.eq('paragraph');\n              expect(data.blocks[1].data.text).to.eq('Second block');\n            });\n        });\n    });\n\n    it('should parse pattern', function () {\n      cy.createEditor({\n        tools: {\n          image: Image,\n        },\n      });\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-block')\n        .click()\n        .paste({\n          // eslint-disable-next-line @typescript-eslint/naming-convention\n          'text/plain': 'https://codex.so/public/app/img/external/codex2x.png',\n        });\n\n      cy.get('[data-cy=editorjs]')\n        // In Edge test are performed slower, so we need to increase timeout to wait until image is loaded on the page\n        .get('img', { timeout: 10000 })\n        .should('have.attr', 'src', 'https://codex.so/public/app/img/external/codex2x.png');\n    });\n\n    it('should not prevent default behaviour if block\\'s paste config equals false', function () {\n      const onPasteStub = cy.stub().as('onPaste');\n\n      /**\n       * Tool with disabled preventing default behavior of onPaste event\n       */\n      class BlockToolWithPasteHandler implements BlockTool {\n        public static pasteConfig = false;\n\n        /**\n         * Render block\n         */\n        public render(): HTMLElement {\n          const block = $.make('div', 'ce-block-with-disabled-prevent-default', {\n            contentEditable: 'true',\n          });\n\n          block.addEventListener('paste', onPasteStub);\n\n          return block;\n        }\n\n        /**\n         * Save data method\n         */\n        public save(): BlockToolData {\n          return {};\n        }\n      }\n\n      cy.createEditor({\n        tools: {\n          blockToolWithPasteHandler: BlockToolWithPasteHandler,\n        },\n      })\n        .as('editorInstanceWithBlockToolWithPasteHandler');\n\n      cy.get('@editorInstanceWithBlockToolWithPasteHandler')\n        .render({\n          blocks: [\n            {\n              type: 'blockToolWithPasteHandler',\n              data: {},\n            },\n          ],\n        })\n        .wait(100);\n\n      cy.get('@editorInstanceWithBlockToolWithPasteHandler')\n        .get('div.ce-block-with-disabled-prevent-default')\n        .click()\n        .paste({\n          // eslint-disable-next-line @typescript-eslint/naming-convention\n          'text/plain': 'Hello',\n        });\n\n      cy.get('@onPaste')\n        .should('have.been.calledWithMatch', {\n          defaultPrevented: false,\n        });\n    });\n  });\n\n  context('copying', function () {\n    it('should copy inline fragment', function () {\n      cy.createEditor({});\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-block')\n        .click()\n        .type('Some text{selectall}')\n        .copy()\n        .then(clipboardData => {\n          /**\n           * As no blocks selected, clipboard data will be empty as will be handled by browser\n           */\n          expect(clipboardData).to.be.empty;\n        });\n    });\n\n    it('should copy several blocks', function () {\n      cy.createEditor({});\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-block')\n        .click()\n        .type('First block{enter}');\n\n      cy.get('[data-cy=editorjs')\n        .get('div.ce-block')\n        .next()\n        .type('Second block')\n        .type('{movetostart}')\n        .trigger('keydown', {\n          shiftKey: true,\n          keyCode: _.keyCodes.UP,\n        })\n        .copy()\n        .then(clipboardData => {\n          expect(clipboardData['text/html']).to.match(/<p>First block(<br>)?<\\/p><p>Second block(<br>)?<\\/p>/);\n          expect(clipboardData['text/plain']).to.eq(`First block\\n\\nSecond block`);\n\n          /**\n           * Need to wait for custom data as it is set asynchronously\n           */\n          cy.wait(0).then(function () {\n            expect(clipboardData['application/x-editor-js']).not.to.be.undefined;\n\n            const data = JSON.parse(clipboardData['application/x-editor-js']);\n\n            expect(data[0].tool).to.eq('paragraph');\n            expect(data[0].data.text).to.match(/First block(<br>)?/);\n            expect(data[1].tool).to.eq('paragraph');\n            expect(data[1].data.text).to.match(/Second block(<br>)?/);\n          });\n        });\n    });\n  });\n\n  context('cutting', function () {\n    it('should cut inline fragment', function () {\n      cy.createEditor({});\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-block')\n        .click()\n        .type('Some text{selectall}')\n        .cut()\n        .then(clipboardData => {\n          /**\n           * As no blocks selected, clipboard data will be empty as will be handled by browser\n           */\n          expect(clipboardData).to.be.empty;\n        });\n    });\n\n    it('should cut several blocks', function () {\n      cy.createEditor({\n        data: {\n          blocks: [\n            {\n              type: 'paragraph',\n              data: { text: 'First block' },\n            },\n            {\n              type: 'paragraph',\n              data: { text: 'Second block' },\n            },\n          ],\n        },\n      });\n\n      cy.get('[data-cy=editorjs')\n        .get('div.ce-block')\n        .last()\n        .click()\n        .type('{movetostart}')\n        .trigger('keydown', {\n          shiftKey: true,\n          keyCode: _.keyCodes.UP,\n        })\n        .cut()\n        .then(clipboardData => {\n          expect(clipboardData['text/html']).to.match(/<p>First block(<br>)?<\\/p><p>Second block(<br>)?<\\/p>/);\n          expect(clipboardData['text/plain']).to.eq(`First block\\n\\nSecond block`);\n\n          /**\n           * Need to wait for custom data as it is set asynchronously\n           */\n          cy.wait(0).then(function () {\n            expect(clipboardData['application/x-editor-js']).not.to.be.undefined;\n\n            const data = JSON.parse(clipboardData['application/x-editor-js']);\n\n            expect(data[0].tool).to.eq('paragraph');\n            expect(data[0].data.text).to.match(/First block(<br>)?/);\n            expect(data[1].tool).to.eq('paragraph');\n            expect(data[1].data.text).to.match(/Second block(<br>)?/);\n          });\n        });\n\n      cy.get('[data-cy=editorjs]')\n        .should('not.contain', 'First block')\n        .should('not.contain', 'Second block');\n    });\n\n    it('should cut lots of blocks', function () {\n      const numberOfBlocks = 50;\n      const blocks = [];\n\n      for (let i = 0; i < numberOfBlocks; i++) {\n        blocks.push({\n          type: 'paragraph',\n          data: {\n            text: `Block ${i}`,\n          },\n        });\n      }\n\n      cy.createEditor({\n        data: {\n          blocks,\n        },\n      });\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-block')\n        .first()\n        .click()\n        .type('{ctrl+A}')\n        .type('{ctrl+A}')\n        .cut()\n        .then((clipboardData) => {\n          /**\n           * Need to wait for custom data as it is set asynchronously\n           */\n          cy.wait(0).then(function () {\n            expect(clipboardData['application/x-editor-js']).not.to.be.undefined;\n\n            const data = JSON.parse(clipboardData['application/x-editor-js']);\n\n            expect(data.length).to.eq(numberOfBlocks);\n          });\n        });\n    });\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/i18n.cy.ts",
    "content": "import Header from '@editorjs/header';\nimport type { ToolboxConfig } from '../../../types';\n\ndescribe('Editor i18n', () => {\n  context('Toolbox', () => {\n    it('should translate tool title in a toolbox', function () {\n      if (this && this.editorInstance) {\n        this.editorInstance.destroy();\n      }\n      const toolNamesDictionary = {\n        Heading: 'Заголовок',\n      };\n\n      cy.createEditor({\n        tools: {\n          header: Header,\n        },\n        i18n: {\n          messages: {\n            toolNames: toolNamesDictionary,\n          },\n        },\n      }).as('editorInstance');\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-block')\n        .click();\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-toolbar__plus')\n        .click();\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-popover-item[data-item-name=header]')\n        .should('contain.text', toolNamesDictionary.Heading);\n    });\n\n    it('should translate titles of toolbox entries', function () {\n      if (this && this.editorInstance) {\n        this.editorInstance.destroy();\n      }\n      const toolNamesDictionary = {\n        Title1: 'Название 1',\n        Title2: 'Название 2',\n      };\n\n      /**\n       * Tool with several toolbox entries configured\n       */\n      class TestTool {\n        /**\n         * Returns toolbox config as list of entries\n         */\n        public static get toolbox(): ToolboxConfig {\n          return [\n            {\n              title: 'Title1',\n              icon: 'Icon 1',\n            },\n            {\n              title: 'Title2',\n              icon: 'Icon 2',\n            },\n          ];\n        }\n      }\n\n      cy.createEditor({\n        tools: {\n          testTool: TestTool,\n        },\n        i18n: {\n          messages: {\n            toolNames: toolNamesDictionary,\n          },\n        },\n      }).as('editorInstance');\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-block')\n        .click();\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-toolbar__plus')\n        .click();\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-popover-item[data-item-name=testTool]')\n        .first()\n        .should('contain.text', toolNamesDictionary.Title1);\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-popover-item[data-item-name=testTool]')\n        .last()\n        .should('contain.text', toolNamesDictionary.Title2);\n    });\n\n    it('should use capitalized tool name as translation key if toolbox title is missing', function () {\n      if (this && this.editorInstance) {\n        this.editorInstance.destroy();\n      }\n\n      /**\n       * Tool class allowing to test case when capitalized tool name is used as translation key if toolbox title is missing\n       */\n      class TestTool {\n        /**\n         * Returns toolbox config without title\n         */\n        public static get toolbox(): ToolboxConfig {\n          return {\n            title: '',\n            icon: '<svg width=\"17\" height=\"15\" viewBox=\"0 0 336 276\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M291 150V79c0-19-15-34-34-34H79c-19 0-34 15-34 34v42l67-44 81 72 56-29 42 30zm0 52l-43-30-56 30-81-67-66 39v23c0 19 15 34 34 34h178c17 0 31-13 34-29zM79 0h178c44 0 79 35 79 79v118c0 44-35 79-79 79H79c-44 0-79-35-79-79V79C0 35 35 0 79 0z\"/></svg>',\n          };\n        }\n      }\n      const toolNamesDictionary = {\n        TestTool: 'ТестТул',\n      };\n\n      cy.createEditor({\n        tools: {\n          testTool: TestTool,\n        },\n        i18n: {\n          messages: {\n            toolNames: toolNamesDictionary,\n          },\n        },\n      });\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-block')\n        .click();\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-toolbar__plus')\n        .click();\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-popover-item[data-item-name=testTool]')\n        .should('contain.text', toolNamesDictionary.TestTool);\n    });\n  });\n\n  context('Block Tunes', () => {\n    it('should translate tool name in Convert To', () => {\n      const toolNamesDictionary = {\n        Heading: 'Заголовок',\n      };\n\n      cy.createEditor({\n        tools: {\n          header: Header,\n        },\n        i18n: {\n          messages: {\n            toolNames: toolNamesDictionary,\n          },\n        },\n        data: {\n          blocks: [\n            {\n              type: 'paragraph',\n              data: {\n                text: 'Some text',\n                level: 1,\n              },\n            },\n          ],\n        },\n      });\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-block')\n        .click();\n\n      /** Open block tunes menu */\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-block')\n        .click();\n\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-toolbar__settings-btn')\n        .click();\n\n      /** Open \"Convert to\" menu  */\n      cy.get('[data-cy=editorjs]')\n        .get('[data-item-name=convert-to]')\n        .click();\n\n      /** Check item in convert to menu is internationalized */\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-popover--nested .ce-popover-item[data-item-name=header]')\n        .should('contain.text', toolNamesDictionary.Heading);\n    });\n  });\n\n  context('Inline Toolbar', () => {\n    it('should translate tool name in Convert To', () => {\n      const toolNamesDictionary = {\n        Heading: 'Заголовок',\n      };\n\n      cy.createEditor({\n        tools: {\n          header: Header,\n        },\n        i18n: {\n          messages: {\n            toolNames: toolNamesDictionary,\n          },\n        },\n        data: {\n          blocks: [\n            {\n              type: 'paragraph',\n              data: {\n                text: 'Some text',\n                level: 1,\n              },\n            },\n          ],\n        },\n      });\n\n      /** Open Inline Toolbar */\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .selectText('Some text');\n\n      /** Open \"Convert to\" menu  */\n      cy.get('[data-cy=editorjs]')\n        .get('[data-item-name=convert-to]')\n        .click();\n\n      /** Check item in convert to menu is internationalized */\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-popover--nested .ce-popover-item[data-item-name=header]')\n        .should('contain.text', toolNamesDictionary.Heading);\n    });\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/initialization.cy.ts",
    "content": "// eslint-disable-next-line spaced-comment, @typescript-eslint/triple-slash-reference\n/// <reference path=\"../support/index.d.ts\" />\n\ndescribe('Editor basic initialization', () => {\n  describe('Zero-config initialization', () => {\n    /**\n     * In this test suite we use zero (omitted) configuration\n     */\n    const editorConfig = {};\n\n    beforeEach(function () {\n      cy.createEditor(editorConfig).as('editorInstance');\n    });\n\n    afterEach(function () {\n      if (this.editorInstance) {\n        this.editorInstance.destroy();\n      }\n    });\n\n    it('should create a visible UI', () => {\n      /**\n       * Assert if created instance is visible or not.\n       */\n      cy.get('[data-cy=editorjs]')\n        .get('div.codex-editor')\n        .should('be.visible');\n    });\n  });\n\n  describe('Configuration', () => {\n    describe('readOnly', () => {\n      beforeEach(() => {\n        if (this && this.editorInstance) {\n          this.editorInstance.destroy();\n        }\n      });\n\n      it('should create editor without editing ability when true passed', () => {\n        cy.createEditor({\n          readOnly: true,\n        }).as('editorInstance');\n\n        cy.get('[data-cy=editorjs]')\n          .get('div.codex-editor')\n          .get('div.ce-paragraph')\n          .invoke('attr', 'contenteditable')\n          .should('eq', 'false');\n      });\n    });\n\n    describe('style', () => {\n      describe('nonce', () => {\n        it('should add passed nonce as attribute to editor style tag', () => {\n          cy.createEditor({\n            style: {\n              nonce: 'test-nonce',\n            },\n          }).as('editorInstance');\n\n          cy.get('[data-cy=editorjs]')\n            .get('#editor-js-styles')\n            .should('have.attr', 'nonce', 'test-nonce');\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/inline-tools/link.cy.ts",
    "content": "describe('Inline Tool Link', () => {\n  it('should create a link by Enter keydown in input', () => {\n    cy.createEditor({\n      data: {\n        blocks: [\n          {\n            type: 'paragraph',\n            data: {\n              text: 'First block text',\n            },\n          },\n        ],\n      },\n    });\n\n    cy.get('[data-cy=editorjs]')\n      .find('div.ce-block')\n      .click()\n      .type('{selectall}')\n      .wait(200)\n      .type('{ctrl}K');\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-inline-tool-input')\n      .click()\n      .type('https://codex.so')\n      .type('{enter}');\n\n    cy.get('[data-cy=editorjs]')\n      .find('div.ce-block')\n      .find('a')\n      .should('have.attr', 'href', 'https://codex.so');\n  });\n\n  it('should remove fake background on selection change', () => {\n    cy.createEditor({\n      data: {\n        blocks: [\n          {\n            type: 'paragraph',\n            data: {\n              text: 'First block text',\n            },\n          },\n          {\n            type: 'paragraph',\n            data: {\n              text: 'Second block text',\n            },\n          },\n        ],\n      },\n    });\n\n    cy.get('[data-cy=editorjs]')\n      .find('div.ce-block')\n      .first()\n      .click()\n      .type('{selectall}')\n      .wait(200)\n      .type('{ctrl}K');\n\n    cy.get('[data-cy=editorjs]')\n      .find('div.ce-block')\n      .last()\n      .click()\n      .type('{selectall}')\n      .wait(200);\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph span[style]')\n      .should('not.exist');\n  });\n\n  it('should preserve link when applying bold to linked text', () => {\n    cy.createEditor({\n      data: {\n        blocks: [\n          {\n            type: 'paragraph',\n            data: {\n              text: 'Text with link',\n            },\n          },\n        ],\n      },\n    });\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .selectText('Text with link');\n\n    cy.get('[data-cy=editorjs]')\n      .find('[data-item-name=link]')\n      .click();\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-inline-tool-input')\n      .type('https://editorjs.io')\n      .type('{enter}');\n\n    cy.get('[data-cy=editorjs]')\n      .find('div.ce-block')\n      .find('a')\n      .should('have.attr', 'href', 'https://editorjs.io');\n\n    cy.get('[data-cy=editorjs]')\n      .find('div.ce-block')\n      .find('a')\n      .selectText('Text with link');\n\n    cy.get('[data-cy=editorjs]')\n      .find('[data-item-name=bold]')\n      .click();\n\n    cy.get('[data-cy=editorjs]')\n      .find('div.ce-block')\n      .find('a')\n      .should('have.attr', 'href', 'https://editorjs.io')\n      .find('b')\n      .should('exist')\n      .should('contain', 'Text with link');\n  });\n\n  it('should preserve bold and italic when applying link', () => {\n    cy.createEditor({\n      data: {\n        blocks: [\n          {\n            type: 'paragraph',\n            data: {\n              text: 'Bold and italic text',\n            },\n          },\n        ],\n      },\n    });\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .selectText('Bold and italic text');\n    \n    cy.get('[data-cy=editorjs]')\n      .find('[data-item-name=bold]')\n      .click();\n\n    cy.get('[data-cy=editorjs]')\n      .find('div.ce-block')\n      .find('b')\n      .should('exist')\n      .should('contain', 'Bold and italic text');\n    \n    cy.get('[data-cy=editorjs]')\n      .find('div.ce-block')\n      .find('b')\n      .selectText('Bold and italic text');\n\n    cy.get('[data-cy=editorjs]')\n      .find('[data-item-name=italic]')\n      .click();\n\n    cy.get('[data-cy=editorjs]')\n      .find('div.ce-block')\n      .find('b')\n      .should('exist')\n      .find('i')\n      .should('exist')\n      .should('contain', 'Bold and italic text');\n\n    cy.get('[data-cy=editorjs]')\n      .find('div.ce-block')\n      .find('b')\n      .find('i')\n      .selectText('Bold and italic text');\n\n    cy.get('[data-cy=editorjs]')\n      .find('[data-item-name=link]')\n      .click();\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-inline-tool-input')\n      .type('https://editorjs.io')\n      .type('{enter}');\n\n    cy.get('[data-cy=editorjs]')\n      .find('div.ce-block')\n      .find('b')\n      .should('exist')\n      .find('i')\n      .should('exist')\n      .find('a')\n      .should('have.attr', 'href', 'https://editorjs.io')\n      .should('contain', 'Bold and italic text');\n  });\n\n  it('should open a link if it is wrapped in another formatting', () => {\n    cy.createEditor({\n      data: {\n        blocks: [\n          {\n            type: 'paragraph',\n            data: {\n              text: 'Link text',\n            },\n          },\n        ],\n      },\n    });\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .selectText('Link text');\n\n    cy.get('[data-cy=editorjs]')\n      .find('[data-item-name=link]')\n      .click();\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-inline-tool-input')\n      .type('https://test.io/')\n      .type('{enter}');\n\n    cy.get('[data-cy=editorjs]')\n      .find('div.ce-block')\n      .find('a')\n      .selectText('Link text');\n\n    cy.get('[data-cy=editorjs]')\n      .find('[data-item-name=italic]')\n      .click();\n\n    cy.window().then((win) => {\n      cy.stub(win, 'open').as('windowOpen');\n    });\n\n    cy.contains('[data-cy=editorjs] div.ce-block i', 'Link text')\n      .click({ ctrlKey: true });\n\n    cy.get('@windowOpen').should('be.calledWith', 'https://test.io/');\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/modules/BlockEvents/ArrowLeft.cy.ts",
    "content": "import { createEditorWithTextBlocks } from '../../../support/utils/createEditorWithTextBlocks';\nimport ContentlessToolMock from '../../../fixtures/tools/ContentlessTool';\n\ndescribe('Arrow Left', function () {\n  describe('starting whitespaces handling', function () {\n    it('&nbsp;| — should natively move caret over the visible space. Then move to the prev block', function () {\n      createEditorWithTextBlocks([\n        '1',\n        '&nbsp;2',\n      ]);\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .last() // select second block\n        .click()\n        .type('{leftArrow}') // set caret before \"2\"\n        .type('{leftArrow}') // move caret over nbsp\n        .type('{leftArrow}'); // move to the prev block\n\n      /**\n       * Caret is set to the end of the previous block\n       */\n      cy.window()\n        .then((window) => {\n          const selection = window.getSelection();\n          const range = selection.getRangeAt(0);\n\n          cy.get('[data-cy=editorjs]')\n            .find('.ce-paragraph')\n            .should(($block) => {\n              expect($block[0].contains(range.startContainer)).to.be.true;\n              expect(range.startOffset).to.eq(1);\n            });\n        });\n    });\n    it(' | — should ignore invisible space before caret and move caret to the prev block', function () {\n      createEditorWithTextBlocks([\n        '1',\n        ' 2',\n      ]);\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .last() // select second block\n        .click()\n        .type('{leftArrow}') // set caret before \"2\"\n        .type('{leftArrow}'); // move to the prev block\n\n      /**\n       * Caret is set to the end of the previous block\n       */\n      cy.window()\n        .then((window) => {\n          const selection = window.getSelection();\n          const range = selection.getRangeAt(0);\n\n          cy.get('[data-cy=editorjs]')\n            .find('.ce-paragraph')\n            .should(($block) => {\n              expect($block[0].contains(range.startContainer)).to.be.true;\n              expect(range.startOffset).to.eq(1);\n            });\n        });\n    });\n\n    it('<b></b>| — should ignore empty tags before caret and move caret to the prev block', function () {\n      createEditorWithTextBlocks([\n        '1',\n        '<b></b>2',\n      ]);\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .last() // select second block\n        .click()\n        .type('{leftArrow}') // set caret before \"2\"\n        .type('{leftArrow}'); // move to the prev block\n\n      /**\n       * Caret is set to the end of the previous block\n       */\n      cy.window()\n        .then((window) => {\n          const selection = window.getSelection();\n          const range = selection.getRangeAt(0);\n\n          cy.get('[data-cy=editorjs]')\n            .find('.ce-paragraph')\n            .should(($block) => {\n              expect($block[0].contains(range.startContainer)).to.be.true;\n              expect(range.startOffset).to.eq(1);\n            });\n        });\n    });\n    it('<b></b>&nbsp;| — should move caret over the visible space and then to the prev block', function () {\n      createEditorWithTextBlocks([\n        '1',\n        '<b></b>&nbsp;2',\n      ]);\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .last()\n        .click()\n        .type('{leftArrow}') // set caret before \"2\"\n        .type('{leftArrow}') // move caret over nbsp\n        .type('{leftArrow}'); // move to the prev block\n\n      /**\n       * Caret is set to the end of the previous block\n       */\n      cy.window()\n        .then((window) => {\n          const selection = window.getSelection();\n          const range = selection.getRangeAt(0);\n\n          cy.get('[data-cy=editorjs]')\n            .find('.ce-paragraph')\n            .should(($block) => {\n              expect($block[0].contains(range.startContainer)).to.be.true;\n              expect(range.startOffset).to.eq(1);\n            });\n        });\n    });\n\n    it('&nbsp;<b></b>| — should ignore empty tag and move caret over the visible space. Then move to the prev block', function () {\n      createEditorWithTextBlocks([\n        '1',\n        '<b></b>&nbsp;2',\n      ]);\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .last()\n        .click()\n        .type('{leftArrow}') // set caret before \"2\"\n        .type('{leftArrow}') // ignore empty tag and move caret over nbsp\n        .type('{leftArrow}'); // move to the prev block\n\n      /**\n       * Caret is set to the end of the previous block\n       */\n      cy.window()\n        .then((window) => {\n          const selection = window.getSelection();\n          const range = selection.getRangeAt(0);\n\n          cy.get('[data-cy=editorjs]')\n            .find('.ce-paragraph')\n            .should(($block) => {\n              expect($block[0].contains(range.startContainer)).to.be.true;\n              expect(range.startOffset).to.eq(1);\n            });\n        });\n    });\n\n    it(' &nbsp;| — should move caret over the visible space. Then move to the prev block', function () {\n      createEditorWithTextBlocks([\n        '1',\n        ' &nbsp;2',\n      ]);\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .last()\n        .click()\n        .type('{leftArrow}') // set caret before \"2\"\n        .type('{leftArrow}') // remove nbsp\n        .type('{leftArrow}'); // ignore regular space and move to the prev block\n\n      /**\n       * Caret is set to the end of the previous block\n       */\n      cy.window()\n        .then((window) => {\n          const selection = window.getSelection();\n          const range = selection.getRangeAt(0);\n\n          cy.get('[data-cy=editorjs]')\n            .find('.ce-paragraph')\n            .should(($block) => {\n              expect($block[0].contains(range.startContainer)).to.be.true;\n              expect(range.startOffset).to.eq(1);\n            });\n        });\n    });\n  });\n\n  /**\n   * In this test we check case:\n   *\n   * Text Block №1\n   * Delimiter\n   * Text Block №2\n   *\n   * we set caret to the start of the Text Block №2 and press Left Arrow\n   *\n   * Expected: Delimiter is selected\n   *\n   * Then we press Left Arrow again\n   *\n   * Expected: Caret is set to the end of the Text Block №1\n   */\n  it('should move caret to the prev block if currently focused block is contentless (Delimiter)', function () {\n    cy.createEditor({\n      tools: {\n        delimiter: ContentlessToolMock,\n      },\n      data: {\n        blocks: [\n          {\n            id: 'block1',\n            type: 'paragraph',\n            data: {\n              text: '1',\n            },\n          },\n          {\n            id: 'block2',\n            type: 'delimiter',\n            data: {},\n          },\n          {\n            id: 'block3',\n            type: 'paragraph',\n            data: {\n              text: '2',\n            },\n          },\n        ],\n      },\n    });\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .last()\n      .as('thirdBlock')\n      .click()\n      .type('{moveToStart}') // set caret before \"2\"\n      .type('{leftArrow}'); // navigate to the Delimiter\n\n    /**\n     * We navigated to the Delimiter and it is highlighted\n     */\n    cy.get('[data-cy=editorjs]')\n      .find('div[data-cy-type=contentless-tool]')\n      .parents('.ce-block')\n      .as('delimiterBlock')\n      .should('have.class', 'ce-block--selected');\n\n    /**\n     * Now press Left again and we should be navigated to the end of the previous block\n     */\n    cy.get('@thirdBlock')\n      .type('{leftArrow}');\n\n    /**\n     * Delimiter is not selected anymore\n     */\n    cy.get('@delimiterBlock')\n      .should('not.have.class', 'ce-block--selected');\n\n    /**\n     * Caret is set to the end of the first block\n     */\n    cy.window()\n      .then((window) => {\n        const selection = window.getSelection();\n        const range = selection.getRangeAt(0);\n\n        cy.get('[data-cy=editorjs]')\n          .find('.ce-paragraph')\n          .first()\n          .should(($block) => {\n            expect($block[0].contains(range.startContainer)).to.be.true;\n            expect(range.startOffset).to.eq(1);\n          });\n      });\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/modules/BlockEvents/ArrowRight.cy.ts",
    "content": "import { createEditorWithTextBlocks } from '../../../support/utils/createEditorWithTextBlocks';\nimport ContentlessToolMock from '../../../fixtures/tools/ContentlessTool';\n\ndescribe('Arrow Right', function () {\n  describe('starting whitespaces handling', function () {\n    it('|&nbsp; — should natively move caret over the visible space. Then move to the next block', function () {\n      createEditorWithTextBlocks([\n        '1&nbsp;',\n        '2',\n      ]);\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .first() // select first block\n        .as('firstBlock')\n        .click()\n        .type('{moveToStart}')\n        .type('{rightArrow}') // set caret after \"1\"\n        .type('{rightArrow}') // move caret over nbsp\n        .type('{rightArrow}'); // move to the next block\n\n      /**\n       * Caret is set to the start of the next block\n       */\n      cy.window()\n        .then((window) => {\n          const selection = window.getSelection();\n          const range = selection.getRangeAt(0);\n\n          cy.get('[data-cy=editorjs]')\n            .find('.ce-paragraph')\n            .last()\n            .should(($block) => {\n              expect($block[0].contains(range.startContainer)).to.be.true;\n              expect(range.startOffset).to.eq(0);\n            });\n        });\n    });\n\n    it('\"| \" — should ignore invisible space after caret and move caret to the next block', function () {\n      createEditorWithTextBlocks([\n        '1 ',\n        '2',\n      ]);\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .first()\n        .click()\n        .type('{moveToStart}')\n        .type('{rightArrow}') // set caret after \"1\"\n        .type('{rightArrow}'); // ignore \" \" and move to the next block\n\n      /**\n       * Caret is set to the start of the next block\n       */\n      cy.window()\n        .then((window) => {\n          const selection = window.getSelection();\n          const range = selection.getRangeAt(0);\n\n          cy.get('[data-cy=editorjs]')\n            .find('.ce-paragraph')\n            .last()\n            .should(($block) => {\n              expect($block[0].contains(range.startContainer)).to.be.true;\n              expect(range.startOffset).to.eq(0);\n            });\n        });\n    });\n\n    it('|<b></b> — should ignore empty tags after caret and move caret to the next block', function () {\n      createEditorWithTextBlocks([\n        '1<b></b>',\n        '2',\n      ]);\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .first()\n        .click()\n        .type('{moveToStart}')\n        .type('{rightArrow}') // set caret after \"1\"\n        .type('{rightArrow}'); // ignore empty tag and move to the next block\n\n      /**\n       * Caret is set to the start of the next block\n       */\n      cy.window()\n        .then((window) => {\n          const selection = window.getSelection();\n          const range = selection.getRangeAt(0);\n\n          cy.get('[data-cy=editorjs]')\n            .find('.ce-paragraph')\n            .last()\n            .should(($block) => {\n              expect($block[0].contains(range.startContainer)).to.be.true;\n              expect(range.startOffset).to.eq(0);\n            });\n        });\n    });\n    it('|&nbsp;<b></b> — should move caret over the visible space and then to the next block', function () {\n      createEditorWithTextBlocks([\n        '1&nbsp;<b></b>',\n        '2',\n      ]);\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .first()\n        .click()\n        .type('{moveToStart}')\n        .type('{rightArrow}') // set caret after \"1\"\n        .type('{rightArrow}') // move caret over nbsp\n        .type('{rightArrow}'); // move to the next block\n\n      /**\n       * Caret is set to the start of the next block\n       */\n      cy.window()\n        .then((window) => {\n          const selection = window.getSelection();\n          const range = selection.getRangeAt(0);\n\n          cy.get('[data-cy=editorjs]')\n            .find('.ce-paragraph')\n            .last()\n            .should(($block) => {\n              expect($block[0].contains(range.startContainer)).to.be.true;\n              expect(range.startOffset).to.eq(0);\n            });\n        });\n    });\n\n    it('|<b></b>&nbsp; — should ignore empty tag and move caret over the visible space. Then move to the next block', function () {\n      createEditorWithTextBlocks([\n        '1<b></b>&nbsp;',\n        '2',\n      ]);\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .first()\n        .click()\n        .type('{moveToStart}')\n        .type('{rightArrow}') // set caret after \"1\"\n        .type('{rightArrow}') // ignore empty tag and move caret over nbsp\n        .type('{rightArrow}'); // move to the next block\n\n      /**\n       * Caret is set to the start of the next block\n       */\n      cy.window()\n        .then((window) => {\n          const selection = window.getSelection();\n          const range = selection.getRangeAt(0);\n\n          cy.get('[data-cy=editorjs]')\n            .find('.ce-paragraph')\n            .last()\n            .should(($block) => {\n              expect($block[0].contains(range.startContainer)).to.be.true;\n              expect(range.startOffset).to.eq(0);\n            });\n        });\n    });\n\n    it('\"|&nbsp; \" — should move caret over the visible space. Then ignore a trailing space and move to the next block', function () {\n      createEditorWithTextBlocks([\n        '1&nbsp; ',\n        '2',\n      ]);\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .first()\n        .click()\n        .type('{moveToStart}')\n        .type('{rightArrow}') // set caret after \"1\"\n        .type('{rightArrow}') // move caret over nbsp\n        .type('{rightArrow}'); // ignore \" \" and move to the next block\n\n      /**\n       * Caret is set to the start of the next block\n       */\n      cy.window()\n        .then((window) => {\n          const selection = window.getSelection();\n          const range = selection.getRangeAt(0);\n\n          cy.get('[data-cy=editorjs]')\n            .find('.ce-paragraph')\n            .last()\n            .should(($block) => {\n              expect($block[0].contains(range.startContainer)).to.be.true;\n              expect(range.startOffset).to.eq(0);\n            });\n        });\n    });\n  });\n\n  /**\n   * In this test we check case:\n   *\n   * Text Block №1\n   * Delimiter\n   * Text Block №2\n   *\n   * we set caret to the end of the Text Block №1 and press Right Arrow\n   *\n   * Expected: Delimiter is selected\n   *\n   * Then we press Right Arrow again\n   *\n   * Expected: Caret is set to the start of the Text Block №2\n   */\n  it('should move caret to the next block if currently focused block is contentless (Delimiter)', function () {\n    cy.createEditor({\n      tools: {\n        delimiter: ContentlessToolMock,\n      },\n      data: {\n        blocks: [\n          {\n            id: 'block1',\n            type: 'paragraph',\n            data: {\n              text: '1',\n            },\n          },\n          {\n            id: 'block2',\n            type: 'delimiter',\n            data: {},\n          },\n          {\n            id: 'block3',\n            type: 'paragraph',\n            data: {\n              text: '2',\n            },\n          },\n        ],\n      },\n    });\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .first()\n      .as('firstBlock')\n      .click() // caret at the end\n      .type('{rightArrow}'); // navigate to the Delimiter\n\n    /**\n     * We navigated to the Delimiter and it is highlighted\n     */\n    cy.get('[data-cy=editorjs]')\n      .find('div[data-cy-type=contentless-tool]')\n      .parents('.ce-block')\n      .as('delimiterBlock')\n      .should('have.class', 'ce-block--selected');\n\n    /**\n     * Now press Right again and we should be navigated to the start of the next block\n     */\n    cy.get('@firstBlock')\n      .type('{rightArrow}');\n\n    /**\n     * Delimiter is not selected anymore\n     */\n    cy.get('@delimiterBlock')\n      .should('not.have.class', 'ce-block--selected');\n\n    /**\n     * Caret is set to the start of the next block\n     */\n    cy.window()\n      .then((window) => {\n        const selection = window.getSelection();\n        const range = selection.getRangeAt(0);\n\n        cy.get('[data-cy=editorjs]')\n          .find('.ce-paragraph')\n          .last()\n          .should(($block) => {\n            expect($block[0].contains(range.startContainer)).to.be.true;\n            expect(range.startOffset).to.eq(0);\n          });\n      });\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/modules/BlockEvents/Backspace.cy.ts",
    "content": "import type EditorJS from '../../../../../types/index';\nimport { SimpleHeader } from '../../../fixtures/tools/SimpleHeader';\nimport type { ConversionConfig } from '../../../../../types/index';\nimport { createEditorWithTextBlocks } from '../../../support/utils/createEditorWithTextBlocks';\n\ndescribe('Backspace keydown', function () {\n  describe('starting whitespaces handling', function () {\n    it('&nbsp;| — should delete visible space', function () {\n      createEditorWithTextBlocks([\n        '1',\n        '&nbsp;2',\n      ]);\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .last()\n        .click()\n        .type('{leftArrow}') // set caret before \"2\"\n        .type('{backspace}');\n\n      cy.get('[data-cy=editorjs]')\n        .find('div.ce-block')\n        .last()\n        .should('have.text', '2');\n    });\n    it(' | — should ignore invisible space before caret and handle it like regular backspace case (merge with previous)', function () {\n      createEditorWithTextBlocks([\n        '1',\n        ' 2',\n      ]);\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .last()\n        .click()\n        .type('{leftArrow}') // set caret before \"2\"\n        .type('{backspace}');\n\n      cy.get('[data-cy=editorjs]')\n        .find('div.ce-block')\n        .last()\n        .should('have.text', '12');\n    });\n    it('<b></b>| — should ignore empty tags before caret and handle it like regular backspace case (merge with previous)', function () {\n      createEditorWithTextBlocks([\n        '1',\n        '<b></b>2',\n      ]);\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .last()\n        .click()\n        .type('{leftArrow}') // set caret before \"2\"\n        .type('{backspace}');\n\n      cy.get('[data-cy=editorjs]')\n        .find('div.ce-block')\n        .last()\n        .should('have.text', '12');\n    });\n    it('<b></b>&nbsp;| — should remove visible space and ignore empty tag', function () {\n      createEditorWithTextBlocks([\n        '1',\n        '<b></b>&nbsp;2',\n      ]);\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .last()\n        .click()\n        .type('{leftArrow}') // set caret before \"2\"\n        .type('{backspace}') // remove nbsp\n        .type('{backspace}'); // ignore empty tag and merge\n\n      cy.get('[data-cy=editorjs]')\n        .find('div.ce-block')\n        .last()\n        .should('have.text', '12');\n    });\n\n    it('&nbsp;<b></b>| — should remove visible space and ignore empty tag', function () {\n      createEditorWithTextBlocks([\n        '1',\n        '<b></b>&nbsp;2',\n      ]);\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .last()\n        .click()\n        .type('{leftArrow}') // set caret before \"2\"\n        .type('{backspace}') // remove nbsp\n        .type('{backspace}'); // ignore empty tag and merge\n\n      cy.get('[data-cy=editorjs]')\n        .find('div.ce-block')\n        .last()\n        .should('have.text', '12');\n    });\n\n    it(' &nbsp;| — should remove visible space and ignore space', function () {\n      createEditorWithTextBlocks([\n        '1',\n        ' &nbsp;2',\n      ]);\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .last()\n        .click()\n        .type('{leftArrow}') // set caret before \"2\"\n        .type('{backspace}') // remove nbsp\n        .type('{backspace}'); // ignore regular space and merge\n\n      cy.get('[data-cy=editorjs]')\n        .find('div.ce-block')\n        .last()\n        .should('have.text', '12');\n    });\n\n    it('&nbsp; &nbsp;| — should delete visible and invisble whitespaces in the abscence of any non whitespace characters', function () {\n      createEditorWithTextBlocks([\n        '1',\n        '&nbsp; &nbsp;',\n      ]);\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .last()\n        .click()\n        .type('{downArrow}')\n        .type('{backspace}')\n        .type('{backspace}')\n        .type('{backspace}')\n        .type('{backspace}');\n\n      cy.get('[data-cy=editorjs]')\n        .find('div.ce-block')\n        .last()\n        .should('have.text', '1');\n    });\n  });\n\n  it('should just delete chars (native behaviour) when some fragment is selected', function () {\n    createEditorWithTextBlocks([\n      'The first block',\n      'The second block',\n    ]);\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .last()\n      .click()\n      .selectText('The ')\n      .type('{backspace}');\n\n    cy.get('[data-cy=editorjs]')\n      .find('div.ce-block')\n      .last()\n      .should('have.text', 'second block');\n  });\n\n  it('should just delete chars (native behaviour) when Caret is not at the start of the Block', function () {\n    createEditorWithTextBlocks([\n      'The first block',\n      'The second block',\n    ]);\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .last()\n      .click() // caret will be at the end of the block\n      .type('{backspace}');\n\n    cy.get('[data-cy=editorjs]')\n      .find('div.ce-block')\n      .last()\n      .should('have.text', 'The second bloc'); // last char is removed\n  });\n\n  it('should navigate previous input when Caret is not at the first input', function () {\n    /**\n     * Mock of tool with several inputs\n     */\n    class ExampleOfToolWithSeveralInputs {\n      /**\n       * Render method mock\n       */\n      public render(): HTMLElement {\n        const container = document.createElement('div');\n        const input = document.createElement('div');\n        const input2 = document.createElement('div');\n\n        container.setAttribute('data-cy', 'quote-tool');\n\n        input.setAttribute('contenteditable', 'true');\n        input2.setAttribute('contenteditable', 'true');\n\n        container.append(input, input2);\n\n        return container;\n      }\n\n      /**\n       * Saving logic is not necessary for this test\n       */\n      // eslint-disable-next-line @typescript-eslint/no-empty-function\n      public save(): void { }\n    }\n\n    cy.createEditor({\n      tools: {\n        quote: ExampleOfToolWithSeveralInputs,\n      },\n      data: {\n        blocks: [\n          {\n            type: 'quote',\n            data: {},\n          },\n        ],\n      },\n    });\n\n    cy.get('[data-cy=editorjs]')\n      .find('[data-cy=quote-tool]')\n      .find('div[contenteditable]')\n      .last()\n      .click()\n      .type('{backspace}');\n\n    cy.get('[data-cy=editorjs]')\n      .find('[data-cy=quote-tool]')\n      .find('div[contenteditable]')\n      .first()\n      .as('firstInput');\n\n    cy.window()\n      .then((window) => {\n        const selection = window.getSelection();\n        const range = selection.getRangeAt(0);\n\n        cy.get('@firstInput').should(($div) => {\n          expect($div[0].contains(range.startContainer)).to.be.true;\n        });\n      });\n  });\n\n  it('should remove previous Block if Caret at the start of the Block and previous Block is empty. Also, should close the Toolbox', function () {\n    cy.createEditor({\n      data: {\n        blocks: [\n          {\n            id: 'block1',\n            type: 'paragraph',\n            data: {\n              text: '', // empty block\n            },\n          },\n          {\n            id: 'block2',\n            type: 'paragraph',\n            data: {\n              text: 'Not empty block',\n            },\n          },\n        ],\n      },\n    }).as('editorInstance');\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .last()\n      .click()\n      .type('{home}') // move caret to the beginning\n      .type('{backspace}');\n\n    cy.get<EditorJS>('@editorInstance')\n      .then(async (editor) => {\n        const { blocks } = await editor.save();\n\n        expect(blocks.length).to.eq(1); // one block has been removed\n        expect(blocks[0].id).to.eq('block2'); // second block is still here\n      });\n  });\n\n  it('should remove current Block if it is empty, but previous is not. Also, should close the Toolbox and set Caret to the end of the prev Block', function () {\n    cy.createEditor({\n      data: {\n        blocks: [\n          {\n            id: 'block1',\n            type: 'paragraph',\n            data: {\n              text: 'Not empty block',\n            },\n          },\n          {\n            id: 'block2',\n            type: 'paragraph',\n            data: {\n              text: '',  // empty block\n            },\n          },\n        ],\n      },\n    }).as('editorInstance');\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .last()\n      .click()\n      .type('{backspace}');\n\n    /**\n     * Current Block has been removed\n     */\n    cy.get<EditorJS>('@editorInstance')\n      .then(async (editor) => {\n        const { blocks } = await editor.save();\n\n        expect(blocks.length).to.eq(1); // one block has been removed\n        expect(blocks[0].id).to.eq('block1'); // second block is still here\n      });\n\n    /**\n     * Caret is set to the end of the previous Block\n     */\n    cy.window()\n      .then((window) => {\n        const selection = window.getSelection();\n        const range = selection.getRangeAt(0);\n\n        cy.get('[data-cy=editorjs]')\n          .find('.ce-paragraph')\n          .should(($block) => {\n            expect($block[0].contains(range.startContainer)).to.be.true;\n            expect(range.startOffset).to.be.eq($block[0].textContent.length);\n          });\n      });\n\n    /**\n     * Toolbox has been closed\n     */\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-toolbar')\n      .should('not.have.class', 'ce-toolbar--opened');\n  });\n\n  it('should merge current Block with the previous one if Caret at the start of the Block and both Blocks are mergeable. Also, should close the Toolbox. Caret should be places in a place of glue', function () {\n    cy.createEditor({\n      data: {\n        blocks: [\n          {\n            id: 'block1',\n            type: 'paragraph',\n            data: {\n              text: 'First block',\n            },\n          },\n          {\n            id: 'block2',\n            type: 'paragraph',\n            data: {\n              text: 'Second block',\n            },\n          },\n        ],\n      },\n    }).as('editorInstance');\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .last()\n      .click()\n      .type('{home}') // move caret to the beginning\n      .type('{backspace}');\n\n    /**\n     * Current Block has been removed\n     */\n    cy.get<EditorJS>('@editorInstance')\n      .then(async (editor) => {\n        const { blocks } = await editor.save();\n\n        expect(blocks.length).to.eq(1); // one block has been removed\n        expect(blocks[0].id).to.eq('block1'); // second block is still here\n        expect(blocks[0].data.text).to.eq('First blockSecond block'); // text has been merged\n      });\n\n    /**\n     * Caret is set to the place of merging\n     */\n    cy.window()\n      .then((window) => {\n        const selection = window.getSelection();\n        const range = selection.getRangeAt(0);\n\n        cy.get('[data-cy=editorjs]')\n          .find('.ce-paragraph')\n          .should(($block) => {\n            expect($block[0].contains(range.startContainer)).to.be.true;\n            range.startContainer.normalize(); // glue merged text nodes\n            expect(range.startOffset).to.be.eq('First block'.length);\n          });\n      });\n\n    /**\n     * Toolbox has been closed\n     */\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-toolbar')\n      .should('not.have.class', 'ce-toolbar--opened');\n  });\n\n  it('should merge blocks of different types (Paragraph -> Header) if they have a valid conversion config. Also, should close the Toolbox. Caret should be places in a place of glue', function () {\n    cy.createEditor({\n      tools: {\n        header: SimpleHeader,\n      },\n      data: {\n        blocks: [\n          {\n            id: 'block1',\n            type: 'header',\n            data: {\n              text: 'First block heading',\n            },\n          },\n          {\n            id: 'block2',\n            type: 'paragraph',\n            data: {\n              text: 'Second block paragraph',\n            },\n          },\n        ],\n      },\n    }).as('editorInstance');\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .last()\n      .click()\n      .type('{home}') // move caret to the beginning\n      .type('{backspace}');\n\n    cy.get<EditorJS>('@editorInstance')\n      .then(async (editor) => {\n        const { blocks } = await editor.save();\n\n        expect(blocks.length).to.eq(1); // one block has been removed\n        expect(blocks[0].id).to.eq('block1'); // second block is still here\n        expect(blocks[0].data.text).to.eq('First block headingSecond block paragraph'); // text has been merged\n      });\n\n    /**\n     * Caret is set to the place of merging\n     */\n    cy.window()\n      .then((window) => {\n        const selection = window.getSelection();\n        const range = selection.getRangeAt(0);\n\n        cy.get('[data-cy=editorjs]')\n          .find('[data-cy=block-wrapper]')\n          .should(($block) => {\n            expect($block[0].contains(range.startContainer)).to.be.true;\n            range.startContainer.normalize(); // glue merged text nodes\n            expect(range.startOffset).to.be.eq('First block heading'.length);\n          });\n      });\n\n    /**\n     * Toolbox has been closed\n     */\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-toolbar')\n      .should('not.have.class', 'ce-toolbar--opened');\n  });\n\n  it('should merge blocks of different types (Header -> Paragraph) if they have a valid conversion config. Also, should close the Toolbox. Caret should be places in a place of glue', function () {\n    cy.createEditor({\n      tools: {\n        header: SimpleHeader,\n      },\n      data: {\n        blocks: [\n          {\n            id: 'block1',\n            type: 'paragraph',\n            data: {\n              text: 'First block paragraph',\n            },\n          },\n          {\n            id: 'block2',\n            type: 'header',\n            data: {\n              text: 'Second block heading',\n            },\n          },\n        ],\n      },\n    }).as('editorInstance');\n\n    cy.get('[data-cy=editorjs]')\n      .find('[data-cy=\"block-wrapper\"][data-id=\"block2\"]')\n      .click()\n      .type('{home}') // move caret to the beginning\n      .type('{backspace}');\n\n    cy.get<EditorJS>('@editorInstance')\n      .then(async (editor) => {\n        const { blocks } = await editor.save();\n\n        expect(blocks.length).to.eq(1); // one block has been removed\n        expect(blocks[0].id).to.eq('block1'); // second block is still here\n        expect(blocks[0].data.text).to.eq('First block paragraphSecond block heading'); // text has been merged\n      });\n\n    /**\n     * Caret is set to the place of merging\n     */\n    cy.window()\n      .then((window) => {\n        const selection = window.getSelection();\n        const range = selection.getRangeAt(0);\n\n        cy.get('[data-cy=editorjs]')\n          .find('[data-cy=block-wrapper]')\n          .should(($block) => {\n            expect($block[0].contains(range.startContainer)).to.be.true;\n            range.startContainer.normalize(); // glue merged text nodes\n            expect(range.startOffset).to.be.eq('First block paragraph'.length);\n          });\n      });\n\n    /**\n     * Toolbox has been closed\n     */\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-toolbar')\n      .should('not.have.class', 'ce-toolbar--opened');\n  });\n\n  it('should simply set Caret to the end of the previous Block if Caret at the start of the Block but Blocks are not mergeable (target Bock is lack of merge() and conversionConfig). Also, should close the Toolbox.', function () {\n    /**\n     * Mock of tool without merge() method\n     */\n    class UnmergeableToolWithoutConversionConfig {\n      /**\n       * Render method mock\n       */\n      public render(): HTMLElement {\n        const container = document.createElement('div');\n\n        container.dataset.cy = 'unmergeable-tool';\n        container.contentEditable = 'true';\n        container.innerHTML = 'Unmergeable not empty tool';\n\n        return container;\n      }\n\n      /**\n       * Saving logic is not necessary for this test\n       */\n      // eslint-disable-next-line @typescript-eslint/no-empty-function\n      public save(): void { }\n    }\n\n    cy.createEditor({\n      tools: {\n        code: UnmergeableToolWithoutConversionConfig,\n      },\n      data: {\n        blocks: [\n          {\n            type: 'code',\n            data: {},\n          },\n          {\n            type: 'paragraph',\n            data: {\n              text: 'Second block',\n            },\n          },\n        ],\n      },\n    });\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .last()\n      .click()\n      .type('{home}')\n      .type('{backspace}');\n\n    cy.get('[data-cy=editorjs]')\n      .find('[data-cy=unmergeable-tool]')\n      .as('firstBlock');\n\n    /**\n     * Caret is set to the previous Block\n     */\n    cy.window()\n      .then((window) => {\n        const selection = window.getSelection();\n        const range = selection.getRangeAt(0);\n\n        cy.get('@firstBlock').should(($div) => {\n          expect($div[0].contains(range.startContainer)).to.be.true;\n        });\n      });\n  });\n\n  it('should simply set Caret to the end of the previous Block if Caret at the start of the Block but Blocks are not mergeable (target Bock is lack of merge() but has the conversionConfig). Also, should close the Toolbox.', function () {\n    /**\n     * Mock of tool without merge() method\n     */\n    class UnmergeableToolWithConversionConfig {\n      /**\n       * Render method mock\n       */\n      public render(): HTMLElement {\n        const container = document.createElement('div');\n\n        container.dataset.cy = 'unmergeable-tool';\n        container.contentEditable = 'true';\n        container.innerHTML = 'Unmergeable not empty tool';\n\n        return container;\n      }\n\n      /**\n       * Saving logic is not necessary for this test\n       */\n      public save(): { key: string } {\n        return {\n          key: 'value',\n        };\n      }\n\n      /**\n       * Mock of the conversionConfig\n       */\n      public static get conversionConfig(): ConversionConfig {\n        return {\n          export: 'key',\n          import: 'key',\n        };\n      }\n    }\n\n    cy.createEditor({\n      tools: {\n        code: UnmergeableToolWithConversionConfig,\n      },\n      data: {\n        blocks: [\n          {\n            type: 'code',\n            data: {},\n          },\n          {\n            type: 'paragraph',\n            data: {\n              text: 'Second block',\n            },\n          },\n        ],\n      },\n    });\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .last()\n      .click()\n      .type('{home}')\n      .type('{backspace}');\n\n    cy.get('[data-cy=editorjs]')\n      .find('[data-cy=unmergeable-tool]')\n      .as('firstBlock');\n\n    /**\n     * Caret is set to the previous Block\n     */\n    cy.window()\n      .then((window) => {\n        const selection = window.getSelection();\n        const range = selection.getRangeAt(0);\n\n        cy.get('@firstBlock').should(($div) => {\n          expect($div[0].contains(range.startContainer)).to.be.true;\n        });\n      });\n  });\n\n  describe('at the start of the first Block', function () {\n    it('should do nothing if Block is not empty', function () {\n      createEditorWithTextBlocks(['The only block. Not empty']);\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .click()\n        .type('{home}')\n        .type('{backspace}');\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .should('have.length', 1)\n        .should('have.text', 'The only block. Not empty');\n    });\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/modules/BlockEvents/Delete.cy.ts",
    "content": "import type EditorJS from '../../../../../types/index';\nimport { createEditorWithTextBlocks } from '../../../support/utils/createEditorWithTextBlocks';\n\ndescribe('Delete keydown', function () {\n  describe('ending whitespaces handling', function () {\n    it('|&nbsp; — should delete visible space', function () {\n      createEditorWithTextBlocks([\n        '1&nbsp;',\n        '2',\n      ]);\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .first()\n        .click()\n        .type('{moveToStart}')\n        .type('{rightArrow}') // set caret after \"1\";\n        .type('{del}') // delete visible space\n        .type('{del}') // merge with next block\n\n        .should('have.text', '12');\n    });\n    it('\"| \" — should ignore invisible space after caret and handle it like regular delete case (merge with next)', function () {\n      createEditorWithTextBlocks([\n        '1 ',\n        '2',\n      ]);\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .first()\n        .click()\n        .type('{moveToStart}')\n        .type('{rightArrow}') // set caret after \"1\";\n        .type('{del}');\n\n      cy.get('[data-cy=editorjs]')\n        .find('div.ce-block')\n        .last()\n        .should('have.text', '1 2');\n    });\n    it('|<b></b> — should ignore empty tags after caret and handle it like regular delete case (merge)', function () {\n      createEditorWithTextBlocks([\n        '1<b></b>',\n        '2',\n      ]);\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .first()\n        .click()\n        .type('{moveToStart}')\n        .type('{rightArrow}') // set caret after \"1\";\n        .type('{del}');\n\n      cy.get('[data-cy=editorjs]')\n        .find('div.ce-block')\n        .last()\n        .should('have.text', '12');\n    });\n    it('|&nbsp;<b></b> — should remove visible space and ignore empty tag', function () {\n      createEditorWithTextBlocks([\n        '1&nbsp;<b></b>',\n        '2',\n      ]);\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .first()\n        .click()\n        .type('{moveToStart}')\n        .type('{rightArrow}') // set caret after \"1\";\n        .type('{del}') // remove nbsp\n        .type('{del}'); // ignore empty tag and merge\n\n      cy.get('[data-cy=editorjs]')\n        .find('div.ce-block')\n        .last()\n        .should('have.text', '12');\n    });\n\n    it('|<b></b>&nbsp; — should remove visible space and ignore empty tag', function () {\n      createEditorWithTextBlocks([\n        '1<b></b>&nbsp;',\n        '2',\n      ]);\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .first()\n        .click()\n        .type('{moveToStart}')\n        .type('{rightArrow}') // set caret after \"1\";\n        .type('{del}') // remove nbsp\n        .type('{del}'); // ignore empty tag and merge\n\n      cy.get('[data-cy=editorjs]')\n        .find('div.ce-block')\n        .last()\n        .should('have.text', '12');\n    });\n\n    it('\"|&nbsp; \" — should remove visible space and ignore space', function () {\n      createEditorWithTextBlocks([\n        '1&nbsp; ',\n        '2',\n      ]);\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .first()\n        .click()\n        .type('{moveToStart}')\n        .type('{rightArrow}') // set caret after \"1\";\n        .type('{del}') // remove nbsp\n        .type('{del}'); // ignore regular space and merge\n\n      cy.get('[data-cy=editorjs]')\n        .find('div.ce-block')\n        .last()\n        /**\n         * In current implementation, we have different behaviour in Firefox:\n         * - Safari, Chrome merge blocks and without whitespace - \"12\"\n         * - Firefox merge blocks and with whitespace - \"1 2\"\n         *\n         * So, we have to check both variants.\n         *\n         * @todo remove this check after fixing the Firefox merge behaviour\n         */\n        .should(($block) => {\n          const text = $block.text();\n\n          expect(text).to.match(/12|1 2/);\n        });\n    });\n  });\n  it('should just delete chars (native behaviour) when some fragment is selected', function () {\n    createEditorWithTextBlocks([\n      'The first block',\n      'The second block',\n    ]);\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .first()\n      .click()\n      .selectText('The ')\n      .type('{del}');\n\n    cy.get('[data-cy=editorjs]')\n      .find('div.ce-block')\n      .first()\n      .should('have.text', 'first block');\n  });\n\n  it('should just delete chars (native behaviour) when Caret is not at the end of the Block', function () {\n    createEditorWithTextBlocks([\n      'The first block',\n      'The second block',\n    ]);\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .first()\n      .click() // caret will be at the end of the block\n      .type('{leftarrow}') // now caret is not at the end\n      .type('{del}');\n\n    cy.get('[data-cy=editorjs]')\n      .find('div.ce-block')\n      .first()\n      .should('have.text', 'The first bloc'); // last char is removed\n  });\n\n  it('should navigate next input when Caret is not at the last input', function () {\n    /**\n     * Mock of tool with several inputs\n     */\n    class ExampleOfToolWithSeveralInputs {\n      /**\n       * Render method mock\n       */\n      public render(): HTMLElement {\n        const container = document.createElement('div');\n        const input = document.createElement('div');\n        const input2 = document.createElement('div');\n\n        container.setAttribute('data-cy', 'quote-tool');\n\n        input.setAttribute('contenteditable', 'true');\n        input2.setAttribute('contenteditable', 'true');\n\n        container.append(input, input2);\n\n        return container;\n      }\n\n      /**\n       * Saving logic is not necessary for this test\n       */\n      // eslint-disable-next-line @typescript-eslint/no-empty-function\n      public save(): void {}\n    }\n\n    cy.createEditor({\n      tools: {\n        quote: ExampleOfToolWithSeveralInputs,\n      },\n      data: {\n        blocks: [\n          {\n            type: 'quote',\n            data: {},\n          },\n        ],\n      },\n    });\n\n    cy.get('[data-cy=editorjs]')\n      .find('[data-cy=quote-tool]')\n      .find('div[contenteditable]')\n      .first()\n      .click()\n      .type('{del}');\n\n    cy.get('[data-cy=editorjs]')\n      .find('[data-cy=quote-tool]')\n      .find('div[contenteditable]')\n      .last()\n      .as('secondInput');\n\n    cy.window()\n      .then((window) => {\n        const selection = window.getSelection();\n        const range = selection.getRangeAt(0);\n\n        cy.get('@secondInput').should(($div) => {\n          expect($div[0].contains(range.startContainer)).to.be.true;\n        });\n      });\n  });\n\n  it('should remove next Block if Caret at the end of the Block and next Block is empty. Also, should close the Toolbox', function () {\n    cy.createEditor({\n      data: {\n        blocks: [\n          {\n            id: 'block1',\n            type: 'paragraph',\n            data: {\n              text: 'Not empty block',\n            },\n          },\n          {\n            id: 'block2',\n            type: 'paragraph',\n            data: {\n              text: '', // empty block\n            },\n          },\n        ],\n      },\n    }).as('editorInstance');\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .first()\n      .click()\n      .type('{del}');\n\n    cy.get<EditorJS>('@editorInstance')\n      .then(async (editor) => {\n        const { blocks } = await editor.save();\n\n        expect(blocks.length).to.eq(1); // one block has been removed\n        expect(blocks[0].id).to.eq('block1'); // first block is still here\n      });\n  });\n\n  it('should remove current Block if it is empty, but next is not. Also, should close the Toolbox and set Caret to the start of the next Block', function () {\n    cy.createEditor({\n      data: {\n        blocks: [\n          {\n            id: 'block1',\n            type: 'paragraph',\n            data: {\n              text: '1',\n            },\n          },\n          {\n            id: 'block2',\n            type: 'paragraph',\n            data: {\n              text: 'Not empty block',\n            },\n          },\n        ],\n      },\n    }).as('editorInstance');\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .first()\n      .click()\n      .type('{backspace}') // remove '1' to make block empty\n      .type('{del}');\n\n    /**\n     * Current Block has been removed\n     */\n    cy.get<EditorJS>('@editorInstance')\n      .then(async (editor) => {\n        const { blocks } = await editor.save();\n\n        expect(blocks.length).to.eq(1); // one block has been removed\n        expect(blocks[0].id).to.eq('block2'); // second block is still here\n      });\n\n    /**\n     * Caret is set to the start of the next Block\n     */\n    cy.window()\n      .then((window) => {\n        const selection = window.getSelection();\n        const range = selection.getRangeAt(0);\n\n        cy.get('[data-cy=editorjs]')\n          .find('.ce-paragraph')\n          .should(($block) => {\n            expect($block[0].contains(range.startContainer)).to.be.true;\n            expect(range.startOffset).to.be.eq(0);\n          });\n      });\n\n    /**\n     * Toolbox has been closed\n     */\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-toolbar')\n      .should('not.have.class', 'ce-toolbar--opened');\n  });\n\n  it('should merge current Block with the next one if Caret at the end of the Block and both Blocks are mergeable. Also, should close the Toolbox.', function () {\n    cy.createEditor({\n      data: {\n        blocks: [\n          {\n            id: 'block1',\n            type: 'paragraph',\n            data: {\n              text: 'First block',\n            },\n          },\n          {\n            id: 'block2',\n            type: 'paragraph',\n            data: {\n              text: 'Second block',\n            },\n          },\n        ],\n      },\n    }).as('editorInstance');\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .first()\n      .click()\n      .type('{del}');\n\n    /**\n     * Current Block has been removed\n     */\n    cy.get<EditorJS>('@editorInstance')\n      .then(async (editor) => {\n        const { blocks } = await editor.save();\n\n        expect(blocks.length).to.eq(1); // one block has been removed\n        expect(blocks[0].id).to.eq('block1'); // second block is still here\n        expect(blocks[0].data.text).to.eq('First blockSecond block'); // text has been merged\n      });\n\n    /**\n     * Caret is set to the place of merging\n     */\n    cy.window()\n      .then((window) => {\n        const selection = window.getSelection();\n        const range = selection.getRangeAt(0);\n\n        cy.get('[data-cy=editorjs]')\n          .find('.ce-paragraph')\n          .should(($block) => {\n            expect($block[0].contains(range.startContainer)).to.be.true;\n            range.startContainer.normalize(); // glue merged text nodes\n            expect(range.startOffset).to.be.eq('First block'.length);\n          });\n      });\n\n    /**\n     * Toolbox has been closed\n     */\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-toolbar')\n      .should('not.have.class', 'ce-toolbar--opened');\n  });\n\n  it('should simply set Caret to the start of the next Block if Caret at the end of the Block but Blocks are not mergeable. Also, should close the Toolbox.', function () {\n    /**\n     * Mock of tool without merge method\n     */\n    class ExampleOfUnmergeableTool {\n      /**\n       * Render method mock\n       */\n      public render(): HTMLElement {\n        const container = document.createElement('div');\n\n        container.dataset.cy = 'unmergeable-tool';\n        container.contentEditable = 'true';\n        container.innerHTML = 'Unmergeable not empty tool';\n\n        return container;\n      }\n\n      /**\n       * Saving logic is not necessary for this test\n       */\n      // eslint-disable-next-line @typescript-eslint/no-empty-function\n      public save(): void {}\n    }\n\n    cy.createEditor({\n      tools: {\n        code: ExampleOfUnmergeableTool,\n      },\n      data: {\n        blocks: [\n          {\n            type: 'paragraph',\n            data: {\n              text: 'Second block',\n            },\n          },\n          {\n            type: 'code',\n            data: {},\n          },\n        ],\n      },\n    });\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .click()\n      .type('{del}');\n\n    cy.get('[data-cy=editorjs]')\n      .find('[data-cy=unmergeable-tool]')\n      .as('secondBlock');\n\n    /**\n     * Caret is set to the previous Block\n     */\n    cy.window()\n      .then((window) => {\n        const selection = window.getSelection();\n        const range = selection.getRangeAt(0);\n\n        cy.get('@secondBlock').should(($div) => {\n          expect($div[0].contains(range.startContainer)).to.be.true;\n        });\n      });\n  });\n\n  describe('at the end of the last Block', function () {\n    it('should do nothing', function () {\n      createEditorWithTextBlocks([ 'The only block. Not empty' ]);\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .click()\n        .type('{del}');\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .should('have.length', 1)\n        .should('have.text', 'The only block. Not empty');\n    });\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/modules/BlockEvents/Enter.cy.ts",
    "content": "describe('Enter keydown', function () {\n  it('should split block and remove selected fragment if some text fragment selected', function () {\n    cy.createEditor({\n      data: {\n        blocks: [\n          {\n            type: 'paragraph',\n            data: {\n              text: 'The block with some text',\n            },\n          },\n        ],\n      },\n    });\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .click()\n      .selectText('with so')\n      .wait(0)\n      .type('{enter}');\n\n    cy.get('[data-cy=editorjs]')\n      .find('div.ce-block')\n      .then((blocks) => {\n        /**\n         * Check that there is two blocks after split\n         */\n        expect(blocks.length).to.equal(2);\n\n        /**\n         * Check that selected text fragment has been removed\n         */\n        expect(blocks[0].textContent).to.equal('The block ');\n        expect(blocks[1].textContent).to.equal('me text');\n      });\n  });\n\n  it('should set caret to the new block if it was created after Enter key press at very end of the block', function () {\n    cy.createEditor({\n      data: {\n        blocks: [\n          {\n            type: 'paragraph',\n            data: {\n              text: 'The block with some text',\n            },\n          },\n        ],\n      },\n    });\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .click()\n      .type('{enter}');\n\n    cy.get('[data-cy=editorjs]')\n      .find('div.ce-block')\n      .last()\n      .as('lastBlock');\n\n    cy.window()\n      .then((window) => {\n        const selection = window.getSelection();\n        const range = selection.getRangeAt(0);\n\n        cy.get('@lastBlock').should(($block) => {\n          expect($block[0].contains(range.startContainer)).to.be.true;\n        });\n      });\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/modules/BlockEvents/Slash.cy.ts",
    "content": "describe('Slash keydown', function () {\n  describe('pressed in empty block', function () {\n    it('should add \"/\" in a block and open Toolbox', () => {\n      cy.createEditor({\n        data: {\n          blocks: [\n            {\n              type: 'paragraph',\n              data: {\n                text: '',\n              },\n            },\n          ],\n        },\n      });\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .click()\n        .type('/');\n\n      /**\n       * Block content should contain slash\n       */\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .invoke('text')\n        .should('eq', '/');\n\n      cy.get('[data-cy=\"toolbox\"] .ce-popover__container')\n        .should('be.visible');\n    });\n\n    [\n      'ctrl',\n      'cmd',\n    ].forEach((key) => {\n      it(`should not open Toolbox if Slash pressed with ${key}`, () => {\n        cy.createEditor({\n          data: {\n            blocks: [\n              {\n                type: 'paragraph',\n                data: {\n                  text: '',\n                },\n              },\n            ],\n          },\n        });\n\n        cy.get('[data-cy=editorjs]')\n          .find('.ce-paragraph')\n          .click()\n          .type(`{${key}}/`);\n\n        cy.get('[data-cy=\"toolbox\"] .ce-popover__container')\n          .should('not.be.visible');\n      });\n    });\n  });\n\n  describe('pressed in non-empty block', function () {\n    it('should not open Toolbox and just add the / char', () => {\n      cy.createEditor({\n        data: {\n          blocks: [\n            {\n              type: 'paragraph',\n              data: {\n                text: 'Hello',\n              },\n            },\n          ],\n        },\n      });\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .click()\n        .type('/');\n\n      cy.get('[data-cy=\"toolbox\"] .ce-popover__container')\n        .should('not.be.visible');\n\n      /**\n       * Block content should contain slash\n       */\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .invoke('text')\n        .should('eq', 'Hello/');\n    });\n  });\n\n  describe('pressed outside editor', function () {\n    it('should not modify any text outside editor when text block is selected', () => {\n      cy.createEditor({\n        data: {\n          blocks: [\n            {\n              type: 'paragraph',\n              data: {\n                text: '',\n              },\n            },\n          ],\n        },\n      });\n\n      cy.document().then((doc) => {\n        const title = doc.querySelector('h1');\n\n        if (title) {\n          title.setAttribute('data-cy', 'page-title');\n        }\n      });\n\n      // Step 1\n      // Click on the plus button and select the text option\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .click();\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-toolbar__plus')\n        .click({ force: true });\n      cy.get('[data-cy=\"toolbox\"] .ce-popover__container')\n        .contains('Text')\n        .click();\n\n      // Step 2\n      // Select the 'Editor.js test page' text\n      cy.get('[data-cy=page-title]')\n        .invoke('attr', 'contenteditable', 'true')\n        .click()\n        .type('{selectall}')\n        .invoke('removeAttr', 'contenteditable');\n\n      // Step 3\n      // Press the Slash key\n      cy.get('[data-cy=page-title]')\n        .trigger('keydown', { key: '/',\n          code: 'Slash',\n          which: 191 });\n\n      cy.get('[data-cy=page-title]').should('have.text', 'Editor.js test page');\n    });\n  });\n});\n\ndescribe('CMD+Slash keydown', function () {\n  it('should open Block Tunes', () => {\n    cy.createEditor({\n      data: {\n        blocks: [\n          {\n            type: 'paragraph',\n            data: {\n              text: '',\n            },\n          },\n        ],\n      },\n    });\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .click()\n      .type('{cmd}/');\n\n    cy.get('[data-cy=\"block-tunes\"] .ce-popover__container')\n      .should('be.visible');\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/modules/BlockEvents/Tab.cy.ts",
    "content": "import ToolMock from '../../../fixtures/tools/ToolMock';\n\n/**\n * Mock of tool that contains two inputs\n */\nclass ToolWithTwoInputs extends ToolMock {\n  /**\n   * Create element with two inputs\n   */\n  public render(): HTMLElement {\n    const wrapper = document.createElement('div');\n    const input1 = document.createElement('div');\n    const input2 = document.createElement('div');\n\n    input1.contentEditable = 'true';\n    input2.contentEditable = 'true';\n\n    wrapper.setAttribute('data-cy', 'tool-with-two-inputs');\n\n    wrapper.appendChild(input1);\n    wrapper.appendChild(input2);\n\n    return wrapper;\n  }\n}\n\n/**\n * Mock of tool without inputs\n */\nclass ContentlessTool extends ToolMock {\n  public static contentless = true;\n  /**\n   * Create element without inputs\n   */\n  public render(): HTMLElement {\n    const wrapper = document.createElement('div');\n\n    wrapper.setAttribute('data-cy', 'contentless-tool');\n\n    wrapper.textContent = '***';\n\n    return wrapper;\n  }\n}\n\n/**\n * Time to wait for caret to finish moving\n */\nconst CARET_MOVE_TIME = 100;\n\ndescribe('Tab keydown', function () {\n  it('should focus next Block if Block contains only one input', () => {\n    cy.createEditor({\n      data: {\n        blocks: [\n          {\n            type: 'paragraph',\n            data: {\n              text: 'first paragraph',\n            },\n          },\n          {\n            type: 'paragraph',\n            data: {\n              text: 'second paragraph',\n            },\n          },\n        ],\n      },\n    });\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .first()\n      .click()\n      .trigger('keydown', { keyCode: 9 })\n      .wait(CARET_MOVE_TIME);\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .last()\n      .then(($secondBlock) => {\n        const editorWindow = $secondBlock.get(0).ownerDocument.defaultView;\n        const selection = editorWindow.getSelection();\n\n        const range = selection.getRangeAt(0);\n\n        /**\n         * Check that second block contains range\n         */\n        expect(range.startContainer.parentElement).to.equal($secondBlock.get(0));\n      });\n  });\n\n  it('should focus next input if Block contains several inputs', () => {\n    cy.createEditor({\n      tools: {\n        toolWithTwoInputs: {\n          class: ToolWithTwoInputs,\n        },\n      },\n      data: {\n        blocks: [\n          {\n            type: 'toolWithTwoInputs',\n            data: {},\n          },\n          {\n            type: 'paragraph',\n            data: {\n              text: 'second paragraph',\n            },\n          },\n        ],\n      },\n    });\n\n    cy.get('[data-cy=tool-with-two-inputs]')\n      .find('[contenteditable=true]')\n      .first()\n      .click()\n      .trigger('keydown', { keyCode: 9 })\n      .wait(CARET_MOVE_TIME);\n\n    cy.get('[data-cy=tool-with-two-inputs]')\n      .find('[contenteditable=true]')\n      .last()\n      .then(($secondInput) => {\n        const editorWindow = $secondInput.get(0).ownerDocument.defaultView;\n        const selection = editorWindow.getSelection();\n\n        const range = selection.getRangeAt(0);\n\n        /**\n         * Check that second block contains range\n         */\n        expect(range.startContainer).to.equal($secondInput.get(0));\n      });\n  });\n\n  it('should highlight next Block if it does not contain any inputs (contentless Block)', () => {\n    cy.createEditor({\n      tools: {\n        contentlessTool: {\n          class: ContentlessTool,\n        },\n      },\n      data: {\n        blocks: [\n          {\n            type: 'paragraph',\n            data: {\n              text: 'second paragraph',\n            },\n          },\n          {\n            type: 'contentlessTool',\n            data: {},\n          },\n          {\n            type: 'paragraph',\n            data: {\n              text: 'third paragraph',\n            },\n          },\n        ],\n      },\n    });\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .first()\n      .click()\n      .trigger('keydown', { keyCode: 9 })\n      .wait(CARET_MOVE_TIME);\n\n    cy.get('[data-cy=contentless-tool]')\n      .parents('.ce-block')\n      .should('have.class', 'ce-block--selected');\n  });\n\n  it('should focus next input after Editor when pressed in last Block', () => {\n    cy.createEditor({});\n\n    /**\n     * Add regular input after Editor\n     */\n    cy.window()\n      .then((window) => {\n        const input = window.document.createElement('input');\n\n        input.setAttribute('data-cy', 'regular-input');\n\n        window.document.body.appendChild(input);\n      });\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .click()\n      .tab();\n\n    cy.get('[data-cy=regular-input]')\n      .should('have.focus');\n  });\n});\n\ndescribe('Shift+Tab keydown', function () {\n  it('should focus previous Block if Block contains only one input', () => {\n    cy.createEditor({\n      data: {\n        blocks: [\n          {\n            type: 'paragraph',\n            data: {\n              text: 'first paragraph',\n            },\n          },\n          {\n            type: 'paragraph',\n            data: {\n              text: 'second paragraph',\n            },\n          },\n        ],\n      },\n    });\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .last()\n      .click()\n      .trigger('keydown', {\n        keyCode: 9,\n        shiftKey: true,\n      })\n      .wait(CARET_MOVE_TIME);\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .first()\n      .then(($firstBlock) => {\n        const editorWindow = $firstBlock.get(0).ownerDocument.defaultView;\n        const selection = editorWindow.getSelection();\n\n        const range = selection.getRangeAt(0);\n\n        /**\n         * Check that second block contains range\n         */\n        expect(range.startContainer.parentElement).to.equal($firstBlock.get(0));\n      });\n  });\n\n  it('should focus previous input if Block contains several inputs', () => {\n    cy.createEditor({\n      tools: {\n        toolWithTwoInputs: {\n          class: ToolWithTwoInputs,\n        },\n      },\n      data: {\n        blocks: [\n          {\n            type: 'paragraph',\n            data: {\n              text: 'second paragraph',\n            },\n          },\n          {\n            type: 'toolWithTwoInputs',\n            data: {},\n          },\n        ],\n      },\n    });\n\n    cy.get('[data-cy=tool-with-two-inputs]')\n      .find('[contenteditable=true]')\n      .last()\n      .click()\n      .trigger('keydown', {\n        keyCode: 9,\n        shiftKey: true,\n      })\n      .wait(CARET_MOVE_TIME);\n\n    cy.get('[data-cy=tool-with-two-inputs]')\n      .find('[contenteditable=true]')\n      .first()\n      .then(($firstInput) => {\n        const editorWindow = $firstInput.get(0).ownerDocument.defaultView;\n        const selection = editorWindow.getSelection();\n\n        const range = selection.getRangeAt(0);\n\n        /**\n         * Check that second block contains range\n         */\n        expect(range.startContainer).to.equal($firstInput.get(0));\n      });\n  });\n\n  it('should highlight previous Block if it does not contain any inputs (contentless Block)', () => {\n    cy.createEditor({\n      tools: {\n        contentlessTool: {\n          class: ContentlessTool,\n        },\n      },\n      data: {\n        blocks: [\n          {\n            type: 'paragraph',\n            data: {\n              text: 'second paragraph',\n            },\n          },\n          {\n            type: 'contentlessTool',\n            data: {},\n          },\n          {\n            type: 'paragraph',\n            data: {\n              text: 'third paragraph',\n            },\n          },\n        ],\n      },\n    });\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .last()\n      .click()\n      .trigger('keydown', {\n        keyCode: 9,\n        shiftKey: true,\n      })\n      .wait(CARET_MOVE_TIME);\n\n    cy.get('[data-cy=contentless-tool]')\n      .parents('.ce-block')\n      .should('have.class', 'ce-block--selected');\n  });\n\n  it('should focus previous input before Editor when pressed in first Block', () => {\n    cy.createEditor({});\n\n    /**\n     * Add regular input before Editor\n     */\n    cy.window()\n      .then((window) => {\n        const input = window.document.createElement('input');\n\n        input.setAttribute('data-cy', 'regular-input');\n\n        window.document.body.insertBefore(input, window.document.body.firstChild);\n      });\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .click()\n      .tab({ shift: true });\n\n    cy.get('[data-cy=regular-input]')\n      .should('have.focus');\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/modules/InlineToolbar.cy.ts",
    "content": "import Header from '@editorjs/header';\nimport NestedEditor, { NESTED_EDITOR_ID } from '../../support/utils/nestedEditorInstance';\nimport type { MenuConfig } from '@/types/tools';\n\ndescribe('Inline Toolbar', () => {\n  it('should appear aligned with left coord of selection rect', () => {\n    cy.createEditor({\n      data: {\n        blocks: [\n          {\n            type: 'paragraph',\n            data: {\n              text: 'First block text',\n            },\n          },\n        ],\n      },\n    });\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .selectText('block');\n\n    cy.get('[data-cy=\"inline-toolbar\"] .ce-popover__container')\n      .should('be.visible')\n      .then(($toolbar) => {\n        const editorWindow = $toolbar.get(0).ownerDocument.defaultView;\n        const selection = editorWindow.getSelection();\n\n        const range = selection.getRangeAt(0);\n        const rect = range.getBoundingClientRect();\n\n        expect($toolbar.offset().left).to.be.closeTo(rect.left, 1);\n      });\n  });\n\n  it('should appear aligned with right side of text column when toolbar\\'s width is not fit at right', () => {\n    cy.createEditor({\n      data: {\n        blocks: [\n          {\n            type: 'paragraph',\n            data: {\n              text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor.',\n            },\n          },\n        ],\n      },\n    });\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .as('blockWrapper')\n      .getLineWrapPositions()\n      .then((lineWrapIndexes) => {\n        const firstLineWrapIndex = lineWrapIndexes[0];\n\n        /**\n         * Select last 5 chars of the first line\n         */\n        cy.get('[data-cy=editorjs]')\n          .find('.ce-paragraph')\n          .selectTextByOffset([firstLineWrapIndex - 5, firstLineWrapIndex - 1]);\n      });\n\n    cy.get('[data-cy=\"inline-toolbar\"] .ce-popover__container')\n      .should('be.visible')\n      .then(($toolbar) => {\n        cy.get('@blockWrapper')\n          .then(($blockWrapper) => {\n            const blockWrapperRect = $blockWrapper.get(0).getBoundingClientRect();\n\n            /**\n             * Toolbar should be aligned with right side of text column\n             */\n            expect($toolbar.offset().left + $toolbar.width()).to.closeTo(blockWrapperRect.right, 10);\n          });\n      });\n  });\n\n  it('should be displayed in read-only mode if at least one inline tool of block supports it', () => {\n    cy.createEditor({\n      tools: {\n        header: {\n          class: Header,\n          inlineToolbar: ['bold', 'testTool'],\n        },\n        testTool: {\n          class: class {\n            public static isInline = true;\n            public static isReadOnlySupported = true;\n            // eslint-disable-next-line jsdoc/require-jsdoc\n            public render(): MenuConfig {\n              return {\n                title: 'Test Tool',\n                name: 'test-tool',\n                // eslint-disable-next-line @typescript-eslint/no-empty-function\n                onActivate: () => {},\n              };\n            }\n          },\n        },\n      },\n      readOnly: true,\n      data: {\n        blocks: [\n          {\n            type: 'header',\n            data: {\n              text: 'First block text',\n            },\n          },\n        ],\n      },\n    });\n\n    /** Open Inline Toolbar */\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-header')\n      .selectText('block');\n\n    cy.get('[data-cy=editorjs]')\n      .get('[data-cy=inline-toolbar]')\n      .get('.ce-popover--opened')\n      .as('toolbar')\n      .should('exist');\n\n    cy.get('@toolbar')\n      .get('.ce-popover-item')\n      .should('have.length', 1)\n      .should('have.attr', 'data-item-name', 'test-tool');\n  });\n\n  it('should not submit form nesting editor when inline tool clicked', () => {\n    cy.createEditor({\n      data: {\n        blocks: [\n          {\n            type: 'paragraph',\n            data: {\n              text: 'Some text',\n            },\n          },\n        ],\n      },\n    });\n\n    const onSubmit = cy.stub();\n\n    cy.document().then(doc => {\n      const form = doc.createElement('form');\n\n      form.onsubmit = onSubmit;\n      doc.body.appendChild(form);\n\n      /* Move editor to form */\n      form.appendChild(doc.getElementById('editorjs'));\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .selectText('Some text');\n\n      cy.get('[data-item-name=bold]')\n        .click();\n\n      expect(onSubmit).to.be.not.called;\n    });\n  });\n\n  describe('Conversion toolbar', () => {\n    it('should restore caret after converting of a block', () => {\n      cy.createEditor({\n        tools: {\n          header: {\n            class: Header,\n          },\n        },\n        data: {\n          blocks: [\n            {\n              type: 'paragraph',\n              data: {\n                text: 'Some text',\n              },\n            },\n          ],\n        },\n      });\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .selectText('Some text');\n\n      cy.get('[data-item-name=convert-to]')\n        .click();\n\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-inline-toolbar')\n        .find('.ce-popover-item[data-item-name=header]')\n        .click();\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-header')\n        .should('have.text', 'Some text');\n\n      cy.window()\n        .then((window) => {\n          const selection = window.getSelection();\n\n          expect(selection.rangeCount).to.be.equal(1);\n\n          const range = selection.getRangeAt(0);\n\n          cy.get('[data-cy=editorjs]')\n            .find('.ce-header')\n            .should(($block) => {\n              expect($block[0].contains(range.startContainer)).to.be.true;\n            });\n        });\n    });\n  });\n\n  describe('Nested Editor instance inline toolbar', () => {\n    it('should not close inline toolbar of the nested Editor instance when clicking within that toolbar', () => {\n      cy.createEditor({\n        tools: {\n          nestedEditor: {\n            class: NestedEditor,\n          },\n        },\n        data: {\n          blocks: [\n            {\n              type: 'paragraph',\n              data: {\n                text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',\n              },\n            },\n            {\n              type: 'nestedEditor',\n              data: {\n                text: 'Nunc pellentesque, tortor nec luctus venenatis',\n              },\n            },\n          ],\n        },\n      });\n\n      cy.get(`[data-cy=${NESTED_EDITOR_ID}]`)\n        .find('.ce-paragraph')\n        .selectText('tortor nec luctus');\n\n      cy.get(`[data-cy=${NESTED_EDITOR_ID}]`)\n        .find('[data-item-name=link]')\n        .click();\n\n      // `wait()` function below is required. without it the test will always pass\n      // because cypress types the text in the field without delay, while we need some delay (just like user)\n      // to test the actual case that nested editor inline toolbar is still visible and not closed\n\n      cy.get(`[data-cy=${NESTED_EDITOR_ID}]`)\n        .find('.ce-inline-tool-input')\n        .click()\n        .wait(100)\n        .type('https://editorjs.io');\n\n      cy.get(`[data-cy=${NESTED_EDITOR_ID}]`)\n        .find('.ce-popover__container')\n        .then(($toolbar) => {\n          expect($toolbar).to.be.visible;\n        });\n    });\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/modules/Renderer.cy.ts",
    "content": "import ToolMock from '../../fixtures/tools/ToolMock';\nimport type EditorJS from '../../../../types/index';\n\ndescribe('Renderer module', function () {\n  it('should not cause onChange firing during initial rendering', function () {\n    const config = {\n      data: {\n        blocks: [\n          {\n            type: 'paragraph',\n            data: {\n              text: 'some text',\n            },\n          },\n          {\n            type: 'paragraph',\n            data: {\n              text: 'some other text',\n            },\n          },\n        ],\n      },\n      // eslint-disable-next-line @typescript-eslint/no-empty-function\n      onChange: () => {},\n    };\n\n    cy.createEditor(config)\n      .as('editorInstance');\n\n    cy.spy(config, 'onChange').as('onChange');\n\n    cy.get('@onChange').should('not.be.called');\n  });\n\n  it('should show Stub block if block tool is not registered', function () {\n    cy.createEditor({\n      data: {\n        blocks: [\n          {\n            type: 'paragraph',\n            data: {\n              text: 'some text',\n            },\n          },\n          {\n            type: 'non-existing tool',\n            data: {},\n          },\n          {\n            type: 'paragraph',\n            data: {\n              text: 'some other text',\n            },\n          },\n        ],\n      },\n    })\n      .as('editorInstance');\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-block')\n      .should('have.length', 3);\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-block')\n      .each(($el, index) => {\n        /**\n         * Check that the second block is stub\n         */\n        if (index === 1) {\n          cy.wrap($el)\n            .find('.ce-stub')\n            .should('have.length', 1);\n\n          /**\n           * Tool title displayed\n           */\n          cy.wrap($el)\n            .find('.ce-stub__title')\n            .should('have.text', 'non-existing tool');\n        }\n      });\n  });\n\n  it('should show Stub block if block tool throws error during construction', function () {\n    /**\n     * Mock of tool that triggers error during construction\n     */\n    class ToolWithError extends ToolMock {\n      /**\n       * @param options - tool options\n       */\n      constructor(options) {\n        super(options);\n        throw new Error('Tool error');\n      }\n    }\n\n    cy.createEditor({\n      data: {\n        blocks: [\n          {\n            type: 'paragraph',\n            data: {\n              text: 'some text',\n            },\n          },\n          {\n            type: 'failedTool',\n            data: {},\n          },\n          {\n            type: 'paragraph',\n            data: {\n              text: 'some other text',\n            },\n          },\n        ],\n      },\n      tools: {\n        failedTool: ToolWithError,\n      },\n    })\n      .as('editorInstance');\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-block')\n      .should('have.length', 3);\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-block')\n      .each(($el, index) => {\n        /**\n         * Check that the second block is stub\n         */\n        if (index === 1) {\n          cy.wrap($el)\n            .find('.ce-stub')\n            .should('have.length', 1);\n\n          /**\n           * Tool title displayed\n           */\n          cy.wrap($el)\n            .find('.ce-stub__title')\n            .should('have.text', 'failedTool');\n        }\n      });\n  });\n\n  it('should insert default empty block when [] passed as data.blocks', function () {\n    cy.createEditor({\n      data: {\n        blocks: [],\n      },\n    })\n      .as('editorInstance');\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-block')\n      .should('have.length', 1);\n  });\n\n  it('should insert default empty block when [] passed via blocks.render() API', function () {\n    cy.createEditor({})\n      .as('editorInstance');\n\n    cy.get<EditorJS>('@editorInstance')\n      .then((editor) => {\n        editor.blocks.render({\n          blocks: [],\n        });\n      });\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-block')\n      .should('have.length', 1);\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/modules/Saver.cy.ts",
    "content": "import type EditorJS from '../../../../types/index';\nimport Header from '@editorjs/header';\n\ndescribe('Saver module', function () {\n  describe('save()', function () {\n    it('should correctly save block if there are some 3rd party (eg. browser extensions) nodes inserted into the layout', function () {\n      cy.createEditor({\n        data: {\n          blocks: [\n            {\n              type: 'paragraph',\n              data: {\n                text: 'The block with some text',\n              },\n            },\n          ],\n        },\n      }).then((editor: EditorJS) => {\n        /**\n         * Add some node just like browser extensions doing\n         */\n        const extensionNode = document.createElement('extension-node');\n\n        cy.get('[data-cy=editorjs]')\n          .find('.ce-block__content')\n          .then((blockContent) => {\n            blockContent.append(extensionNode);\n          })\n          .then(async () => {\n            const savedData = await editor.save();\n\n            expect(savedData.blocks.length).to.equal(1);\n            expect(savedData.blocks[0].data.text).to.equal('The block with some text');\n          });\n      });\n    });\n\n    /**\n     * This test case covers Block@detectToolRootChange\n     */\n    it('should correctly save block data if block\\'s main container element have been changed', function () {\n      cy.createEditor({\n        tools: {\n          header: Header,\n        },\n        data: {\n          blocks: [\n            {\n              type: 'header',\n              data: {\n                text: 'The block with some text',\n                level: 1,\n              },\n            },\n          ],\n        },\n      })\n        .as('editorInstance');\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-block')\n        .click();\n\n      cy.get('[data-cy=editorjs]')\n        .get('span.ce-toolbar__settings-btn')\n        .click();\n\n      /**\n       * Change header level\n       */\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-settings .ce-popover-item:nth-child(3)')\n        .click();\n\n      cy.get<EditorJS>('@editorInstance')\n        .then(async (editor) => {\n          const data = await editor.save();\n\n          expect(data.blocks[0].type).to.equal('header');\n          expect(data.blocks[0].data.text).to.equal('The block with some text');\n          // eslint-disable-next-line @typescript-eslint/no-magic-numbers\n          expect(data.blocks[0].data.level).to.equal(3);\n        });\n    });\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/modules/Tools.cy.ts",
    "content": "/* tslint:disable:max-classes-per-file */\n/* eslint-disable @typescript-eslint/no-explicit-any, jsdoc/require-jsdoc */\nimport Tools from '../../../../src/components/modules/tools';\nimport type { EditorConfig } from '../../../../types';\nimport BlockToolAdapter from '../../../../src/components/tools/block';\n\ndescribe('Tools module', () => {\n  const defaultConfig = {\n    tools: {},\n  };\n\n  /**\n   * Construct Tools module for testing purposes\n   *\n   * @param config - Editor config\n   */\n  function constructModule(config: EditorConfig = defaultConfig): Tools {\n    const module = new Tools({\n      config,\n      eventsDispatcher: {},\n    } as any);\n\n    const APIMethods = {\n      // eslint-disable-next-line @typescript-eslint/no-empty-function\n      method(): void {},\n    };\n\n    /**\n     * Module state should be Editor modules, so we mock required ones only\n     */\n    module.state = {\n      API: {\n        getMethodsForTool(): typeof APIMethods {\n          return APIMethods;\n        },\n      },\n    } as any;\n\n    return module;\n  }\n\n  context('.prepare()', () => {\n    it('should return Promise resolved to void', async () => {\n      const module = constructModule();\n\n      let err;\n\n      try {\n        await module.prepare();\n      } catch (e) {\n        err = e;\n      }\n\n      expect(err).to.be.undefined;\n    });\n\n    it('should throw an error if tools config is corrupted', async () => {\n      const module = constructModule({\n        tools: {\n          // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n          // @ts-ignore\n          corruptedTool: 'value',\n        },\n      });\n\n      let err;\n\n      try {\n        await module.prepare();\n      } catch (e) {\n        err = e;\n      }\n\n      expect(err).to.be.instanceOf(Error);\n    });\n\n    // eslint-disable-next-line cypress/no-async-tests\n    it('should call Tools prepare method with user config', async () => {\n      class WithSuccessfulPrepare {\n        // eslint-disable-next-line @typescript-eslint/no-empty-function\n        public static prepare = cy.stub();\n      }\n\n      const config = {\n        property: 'value',\n      };\n\n      const module = constructModule({\n        defaultBlock: 'withSuccessfulPrepare',\n        tools: {\n          withSuccessfulPrepare: {\n            class: WithSuccessfulPrepare as any,\n            config,\n          },\n        },\n      });\n\n      await module.prepare();\n\n      expect(WithSuccessfulPrepare.prepare).to.be.calledWithExactly({\n        toolName: 'withSuccessfulPrepare',\n        config,\n      });\n    });\n  });\n\n  context('collection accessors', () => {\n    let module: Tools;\n\n    beforeEach(async () => {\n      module = constructModule({\n        defaultBlock: 'withoutPrepare',\n        tools: {\n          withSuccessfulPrepare: {\n            class: class {\n              // eslint-disable-next-line @typescript-eslint/no-empty-function\n              public static prepare(): void {}\n            } as any,\n            inlineToolbar: [ 'inlineTool2' ],\n            tunes: [ 'blockTune2' ],\n          },\n          withFailedPrepare: class {\n            public static prepare(): void {\n              throw new Error();\n            }\n          } as any,\n          withoutPrepare: {\n            class: class {} as any,\n            inlineToolbar: false,\n            tunes: false,\n          },\n          blockTool: {\n            class: class {} as any,\n            inlineToolbar: true,\n          },\n          blockToolWithoutSettings: class {} as any,\n          inlineTool: class {\n            public static isInline = true;\n\n            // eslint-disable-next-line @typescript-eslint/no-empty-function\n            public render(): void {}\n\n            // eslint-disable-next-line @typescript-eslint/no-empty-function\n            public surround(): void {}\n\n            // eslint-disable-next-line @typescript-eslint/no-empty-function\n            public checkState(): void {}\n          } as any,\n          inlineTool2: class {\n            public static isInline = true;\n\n            // eslint-disable-next-line @typescript-eslint/no-empty-function\n            public render(): void {}\n\n            // eslint-disable-next-line @typescript-eslint/no-empty-function\n            public surround(): void {}\n\n            // eslint-disable-next-line @typescript-eslint/no-empty-function\n            public checkState(): void {}\n          } as any,\n          /**\n           * This tool will be unavailable as it doesn't have required methods\n           */\n          unavailableInlineTool: class {\n            public static isInline = true;\n          } as any,\n          blockTune: class {\n            public static isTune = true;\n          } as any,\n          blockTune2: class {\n            public static isTune = true;\n          } as any,\n          unavailableBlockTune: class {\n            public static isTune = true;\n\n            public static prepare(): void {\n              throw new Error();\n            }\n          } as any,\n        },\n        inlineToolbar: ['inlineTool2', 'inlineTool'],\n        tunes: ['blockTune2', 'blockTune'],\n      });\n\n      await module.prepare();\n    });\n\n    context('.available', () => {\n      it('should return Map instance', () => {\n        expect(module.available).to.be.instanceOf(Map);\n      });\n\n      it('should contain only ready to use Tools', () => {\n        expect(module.available.has('withSuccessfulPrepare')).to.be.true;\n        expect(module.available.has('withoutPrepare')).to.be.true;\n        expect(module.available.has('withFailedPrepare')).to.be.false;\n        expect(module.available.has('unavailableInlineTool')).to.be.false;\n      });\n    });\n\n    context('.unavailable', () => {\n      it('should return Map instance', () => {\n        expect(module.unavailable).to.be.instanceOf(Map);\n      });\n\n      it('should contain unavailable Tools', () => {\n        expect(module.unavailable.has('withSuccessfulPrepare')).to.be.false;\n        expect(module.unavailable.has('withoutPrepare')).to.be.false;\n        expect(module.unavailable.has('withFailedPrepare')).to.be.true;\n        expect(module.unavailable.has('unavailableInlineTool')).to.be.true;\n      });\n    });\n\n    context('.inlineTools', () => {\n      it('should return Map instance', () => {\n        expect(module.inlineTools).to.be.instanceOf(Map);\n      });\n\n      it('should contain only available Inline Tools', () => {\n        expect(module.inlineTools.has('inlineTool')).to.be.true;\n        expect(module.inlineTools.has('unavailableInlineTool')).to.be.false;\n        expect(Array.from(module.inlineTools.values()).every(tool => tool.isInline())).to.be.true;\n      });\n    });\n\n    context('.blockTools', () => {\n      it('should return Map instance', () => {\n        expect(module.blockTools).to.be.instanceOf(Map);\n      });\n\n      it('should contain only available Block Tools', () => {\n        expect(module.blockTools.has('withSuccessfulPrepare')).to.be.true;\n        expect(module.blockTools.has('withoutPrepare')).to.be.true;\n        expect(module.blockTools.has('withFailedPrepare')).to.be.false;\n        expect(Array.from(module.blockTools.values()).every(tool => tool.isBlock())).to.be.true;\n      });\n\n      it('Block Tools should contain default tunes if no settings is specified', () => {\n        const tool = module.blockTools.get('blockToolWithoutSettings');\n\n        expect(tool.tunes.has('delete')).to.be.true;\n        expect(tool.tunes.has('moveUp')).to.be.true;\n        expect(tool.tunes.has('moveDown')).to.be.true;\n      });\n\n      it('Block Tools should contain default tunes', () => {\n        const tool = module.blockTools.get('blockTool');\n\n        expect(tool.tunes.has('delete')).to.be.true;\n        expect(tool.tunes.has('moveUp')).to.be.true;\n        expect(tool.tunes.has('moveDown')).to.be.true;\n      });\n\n      it('Block Tools should contain tunes in correct order', () => {\n        let tool = module.blockTools.get('blockTool');\n\n        expect(tool.tunes.has('blockTune')).to.be.true;\n        expect(tool.tunes.has('blockTune2')).to.be.true;\n        expect(Array.from(tool.tunes.keys())).to.be.deep.eq(['blockTune2', 'blockTune', 'moveUp', 'delete', 'moveDown']);\n\n        tool = module.blockTools.get('withSuccessfulPrepare');\n\n        expect(tool.tunes.has('blockTune')).to.be.false;\n        expect(tool.tunes.has('blockTune2')).to.be.true;\n\n        tool = module.blockTools.get('withoutPrepare');\n\n        expect(tool.tunes.has('blockTune')).to.be.false;\n        expect(tool.tunes.has('blockTune2')).to.be.false;\n      });\n\n      it('Block Tools should contain inline tools in correct order', () => {\n        let tool = module.blockTools.get('blockTool');\n\n        expect(tool.inlineTools.has('inlineTool')).to.be.true;\n        expect(tool.inlineTools.has('inlineTool2')).to.be.true;\n        expect(Array.from(tool.inlineTools.keys())).to.be.deep.eq(['inlineTool2', 'inlineTool']);\n\n        tool = module.blockTools.get('withSuccessfulPrepare');\n\n        expect(tool.inlineTools.has('inlineTool')).to.be.false;\n        expect(tool.inlineTools.has('inlineTool2')).to.be.true;\n\n        tool = module.blockTools.get('withoutPrepare');\n\n        expect(tool.inlineTools.has('inlineTool')).to.be.false;\n        expect(tool.inlineTools.has('inlineTool2')).to.be.false;\n      });\n    });\n\n    context('.blockTunes', () => {\n      it('should return Map instance', () => {\n        expect(module.blockTunes).to.be.instanceOf(Map);\n      });\n\n      it('should contain only available Block Tunes', () => {\n        expect(module.blockTunes.has('blockTune')).to.be.true;\n        expect(module.blockTunes.has('unavailableBlockTune')).to.be.false;\n        expect(Array.from(module.blockTunes.values()).every(tool => tool.isTune())).to.be.true;\n      });\n    });\n\n    context('.internal', () => {\n      it('should return Map instance', () => {\n        expect(module.internal).to.be.instanceOf(Map);\n      });\n\n      it('should contain only internal tunes', () => {\n        expect(Array.from(module.internal.values()).every(tool => tool.isInternal)).to.be.true;\n      });\n    });\n\n    context('.defaultTools', () => {\n      /**\n       * @todo add check if user provided default tool is not Block Tool\n       */\n      it('should return BlockTool instance', () => {\n        expect(module.defaultTool).to.be.instanceOf(BlockToolAdapter);\n      });\n\n      it('should return default Tool', () => {\n        expect(module.defaultTool.isDefault).to.be.true;\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/modules/Ui.cy.ts",
    "content": "import { createEditorWithTextBlocks } from '../../support/utils/createEditorWithTextBlocks';\nimport type EditorJS from '../../../../types/index';\n\ndescribe('Ui module', function () {\n  describe('documentKeydown', function () {\n    describe('Backspace', function () {\n      it('should remove selected blocks', function () {\n        cy.createEditor({\n          data: {\n            blocks: [\n              {\n                id: 'block1',\n                type: 'paragraph',\n                data: {\n                  text: 'The first block',\n                },\n              },\n              {\n                id: 'block2',\n                type: 'paragraph',\n                data: {\n                  text: 'The second block',\n                },\n              },\n            ],\n          },\n        }).as('editorInstance');\n\n        /**\n         * Select two blocks by shift+down\n         */\n        cy.get('[data-cy=editorjs]')\n          .find('.ce-paragraph')\n          .first()\n          .click()\n          .type('{shift+downArrow}')\n          .type('{backspace}');\n\n\n        cy.get<EditorJS>('@editorInstance')\n          .then(async (editor) => {\n            const { blocks } = await editor.save();\n\n            /**\n             * Actually editor will contain 1 empty block, but save wont return it since it is empty\n             */\n            expect(blocks.length).to.eq(0);\n          });\n      });\n    });\n\n    describe('Delete', function () {\n      it('should remove selected blocks', function () {\n        cy.createEditor({\n          data: {\n            blocks: [\n              {\n                id: 'block1',\n                type: 'paragraph',\n                data: {\n                  text: 'The first block',\n                },\n              },\n              {\n                id: 'block2',\n                type: 'paragraph',\n                data: {\n                  text: 'The second block',\n                },\n              },\n            ],\n          },\n        }).as('editorInstance');\n\n        /**\n         * Select two blocks by shift+down\n         */\n        cy.get('[data-cy=editorjs]')\n          .find('.ce-paragraph')\n          .first()\n          .click()\n          .type('{shift+downArrow}')\n          .type('{del}');\n\n        cy.get<EditorJS>('@editorInstance')\n          .then(async (editor) => {\n            const { blocks } = await editor.save();\n\n            /**\n             * Actually editor will contain 1 empty block, but save wont return it since it is empty\n             */\n            expect(blocks.length).to.eq(0);\n          });\n      });\n    });\n  });\n\n  describe('mousedown', function () {\n    it('should update current block by click on block', function () {\n      createEditorWithTextBlocks([\n        'first block',\n        'second block',\n        'third block',\n      ])\n        .as('editorInstance');\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .eq(1)\n        .click();\n\n      cy.get<EditorJS>('@editorInstance')\n        .then(async (editor) => {\n          const currentBlockIndex = await editor.blocks.getCurrentBlockIndex();\n\n          expect(currentBlockIndex).to.eq(1);\n        });\n    });\n\n    it('(in readonly) should update current block by click on block', function () {\n      createEditorWithTextBlocks([\n        'first block',\n        'second block',\n        'third block',\n      ], {\n        readOnly: true,\n      })\n        .as('editorInstance');\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .eq(1)\n        .click();\n\n      cy.get<EditorJS>('@editorInstance')\n        .then(async (editor) => {\n          const currentBlockIndex = await editor.blocks.getCurrentBlockIndex();\n\n          expect(currentBlockIndex).to.eq(1);\n        });\n    });\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/onchange.cy.ts",
    "content": "import Header from '@editorjs/header';\nimport Code from '@editorjs/code';\nimport ToolMock from '../fixtures/tools/ToolMock';\nimport Delimiter from '@editorjs/delimiter';\nimport { BlockAddedMutationType } from '../../../types/events/block/BlockAdded';\nimport { BlockChangedMutationType } from '../../../types/events/block/BlockChanged';\nimport { BlockRemovedMutationType } from '../../../types/events/block/BlockRemoved';\nimport { BlockMovedMutationType } from '../../../types/events/block/BlockMoved';\nimport type EditorJS from '../../../types/index';\nimport { modificationsObserverBatchTimeout } from '../../../src/components/constants';\n\n/**\n * EditorJS API is passed as the first parameter of the onChange callback\n */\nconst EditorJSApiMock = Cypress.sinon.match.any;\n\n/**\n * @todo Add checks that correct block API object is passed to onChange\n * @todo Add cases for native inputs changes\n * @todo debug onChange firing on Block Tune toggling (see below)\n */\ndescribe('onChange callback', () => {\n  /**\n   * Creates Editor instance\n   *\n   * @param blocks - list of blocks to prefill the editor\n   */\n  function createEditor(blocks = null): void {\n    const config = {\n      tools: {\n        header: Header,\n        code: Code,\n      },\n      onChange: (api, event): void => {\n        console.log('something changed', event);\n      },\n      data: blocks ? {\n        blocks,\n      } : null,\n    };\n\n    cy.spy(config, 'onChange').as('onChange');\n\n    cy.createEditor(config).as('editorInstance');\n  }\n\n  /**\n   * Creates Editor instance with save inside the onChange event.\n   *\n   * @param blocks - list of blocks to prefill the editor\n   */\n  function createEditorWithSave(blocks = null): void {\n    const config = {\n      tools: {\n        header: Header,\n        code: Code,\n        delimiter: Delimiter,\n      },\n      onChange: (api, event): void => {\n        console.log('something changed', event);\n        api.saver.save();\n      },\n      data: blocks ? {\n        blocks,\n      } : null,\n    };\n\n    cy.spy(config, 'onChange').as('onChange');\n\n    cy.createEditor(config).as('editorInstance');\n  }\n\n  it('should batch events when several changes happened at once', () => {\n    createEditor([\n      {\n        type: 'paragraph',\n        data: {\n          text: 'The first paragraph',\n        },\n      },\n    ]);\n\n    cy.get('[data-cy=editorjs]')\n      .get('div.ce-block')\n      .click()\n      .type('change')\n      .type('{enter}');\n\n    cy.get('@onChange').should('be.calledWithBatchedEvents', [\n      {\n        type: BlockChangedMutationType,\n        detail: {\n          index: 0,\n        },\n      },\n      {\n        type: BlockAddedMutationType,\n        detail: {\n          index: 1,\n        },\n      },\n    ]);\n  });\n\n  it('should filter out similar events on batching', () => {\n    createEditor([\n      {\n        type: 'paragraph',\n        data: {\n          text: 'The first paragraph',\n        },\n      },\n    ]);\n\n    cy.get('[data-cy=editorjs]')\n      .get('div.ce-block')\n      .click()\n      .type('first change')\n      // eslint-disable-next-line @typescript-eslint/no-magic-numbers\n      .wait(100)\n      .type('second change');\n\n    cy.get('@onChange').should('be.calledOnce');\n    cy.get('@onChange').should('be.calledWithMatch', EditorJSApiMock, Cypress.sinon.match({\n      type: BlockChangedMutationType,\n      detail: {\n        target: {\n          name: 'paragraph',\n        },\n        index: 0,\n      },\n    }));\n  });\n\n  it('should be fired with correct index on block insertion above the current (by pressing Enter at the start)', () => {\n    createEditor();\n\n    cy.get('[data-cy=editorjs]')\n      .get('div.ce-block')\n      .click()\n      .type('{enter}');\n\n    cy.get('@onChange').should('be.calledWithMatch', EditorJSApiMock, Cypress.sinon.match({\n      type: BlockAddedMutationType,\n      detail: {\n        target: {\n          name: 'paragraph',\n        },\n        index: 0,\n      },\n    }));\n  });\n\n  it('should be fired with only single \"block-added\" event by pressing Enter at the end of a block', () => {\n    createEditor([ {\n      type: 'paragraph',\n      data: {\n        text: 'some text',\n      },\n    } ]);\n\n    cy.get('[data-cy=editorjs]')\n      .get('div.ce-block')\n      .click()\n      .type('{enter}');\n\n    cy.get('@onChange').should('be.calledOnce');\n    cy.get('@onChange').should('be.calledWithMatch', EditorJSApiMock, Cypress.sinon.match({\n      type: BlockAddedMutationType,\n    }));\n  });\n\n  it('should be fired with correct index on block insertion after the current (by pressing enter at the end)', () => {\n    createEditor([ {\n      type: 'paragraph',\n      data: {\n        text: 'some text',\n      },\n    } ]);\n\n    cy.get('[data-cy=editorjs]')\n      .get('div.ce-block')\n      .click()\n      .type('{enter}');\n\n    cy.get('@onChange').should('be.calledWithMatch', EditorJSApiMock, Cypress.sinon.match({\n      type: BlockAddedMutationType,\n      detail: {\n        index: 1,\n      },\n    }));\n  });\n\n  it('should be fired on typing into block', () => {\n    createEditor();\n\n    cy.get('[data-cy=editorjs]')\n      .get('div.ce-block')\n      .click()\n      .type('some text');\n\n    cy.get('@onChange').should('be.calledWithMatch', EditorJSApiMock, Cypress.sinon.match({\n      type: BlockChangedMutationType,\n      detail: {\n        index: 0,\n      },\n    }));\n  });\n\n  it('should be fired on block insertion with save inside onChange', () => {\n    createEditorWithSave();\n\n    cy.get('[data-cy=editorjs]')\n      .get('div.ce-block')\n      .click();\n\n    cy.get('[data-cy=editorjs]')\n      .get('div.ce-toolbar__plus')\n      .click();\n\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-popover-item[data-item-name=delimiter]')\n      .click();\n\n    cy.get('@onChange').should('be.calledWithBatchedEvents', [\n      {\n        type: BlockRemovedMutationType,\n        detail: {\n          index: 0,\n          target: {\n            name: 'paragraph',\n          },\n        },\n      },\n      {\n        type: BlockAddedMutationType,\n        detail: {\n          index: 0,\n          target: {\n            name: 'delimiter',\n          },\n        },\n      },\n      {\n        type: BlockAddedMutationType,\n        detail: {\n          index: 1,\n          target: {\n            name: 'paragraph',\n          },\n        },\n      },\n    ]);\n  });\n\n  it('should be fired on block replacement for both of blocks', () => {\n    createEditor();\n\n    cy.get('[data-cy=editorjs]')\n      .get('div.ce-block')\n      .click();\n\n    cy.get('[data-cy=editorjs]')\n      .get('div.ce-toolbar__plus')\n      .click();\n\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-popover-item[data-item-name=header]')\n      .click();\n\n    cy.get('@onChange').should('be.calledWithBatchedEvents', [\n      {\n        type: BlockRemovedMutationType,\n        detail: {\n          index: 0,\n          target: {\n            name: 'paragraph',\n          },\n        },\n      },\n      {\n        type: BlockAddedMutationType,\n        detail: {\n          index: 0,\n          target: {\n            name: 'header',\n          },\n        },\n      },\n    ]);\n  });\n\n  it('should be fired on tune modifying', () => {\n    createEditor([\n      {\n        type: 'header',\n        data: {\n          text: 'Header block',\n        },\n      },\n    ]);\n\n    cy.get('[data-cy=editorjs]')\n      .get('div.ce-block')\n      .click();\n\n    cy.get('[data-cy=editorjs]')\n      .get('span.ce-toolbar__settings-btn')\n      .click();\n\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-settings .ce-popover-item:nth-child(4)')\n      .click();\n\n    cy.get('@onChange').should('be.calledWithMatch', EditorJSApiMock, Cypress.sinon.match({\n      type: BlockChangedMutationType,\n      detail: {\n        index: 0,\n        target: {\n          name: 'header',\n        },\n      },\n    }));\n  });\n\n  it('should be fired when block is removed', () => {\n    createEditor([\n      {\n        type: 'paragraph',\n        data: {\n          text: 'some text',\n        },\n      },\n    ]);\n\n    cy.get('[data-cy=editorjs]')\n      .get('div.ce-block')\n      .click();\n\n    cy.get('[data-cy=editorjs]')\n      .get('span.ce-toolbar__settings-btn')\n      .click();\n\n    cy.get('[data-cy=editorjs]')\n      .get('[data-item-name=delete]')\n      .click();\n\n    /** Second click for confirmation */\n    cy.get('[data-cy=editorjs]')\n      .get('[data-item-name=delete]')\n      .click();\n\n    cy.get('@onChange').should('be.calledWithBatchedEvents', [\n      /**\n       * \"block-removed\" fired since we have deleted a block\n       */\n      {\n        type: BlockRemovedMutationType,\n        detail: {\n          index: 0,\n        },\n      },\n      /**\n       * \"block-added\" fired since we have deleted the last block, so the new one is created\n       */\n      {\n        type: BlockAddedMutationType,\n        detail: {\n          index: 0,\n        },\n      },\n    ]);\n  });\n\n  it('should be fired when block is moved', () => {\n    createEditor([\n      {\n        type: 'paragraph',\n        data: {\n          text: 'first block',\n        },\n      },\n      {\n        type: 'paragraph',\n        data: {\n          text: 'second block',\n        },\n      },\n    ]);\n\n    cy.get('[data-cy=editorjs]')\n      .get('div.ce-block')\n      .last()\n      .click();\n\n    cy.get('[data-cy=editorjs]')\n      .get('span.ce-toolbar__settings-btn')\n      .click();\n\n    cy.get('[data-cy=editorjs]')\n      .get('[data-item-name=move-up]')\n      .click();\n\n    cy.get('@onChange').should('be.calledWithMatch', EditorJSApiMock, Cypress.sinon.match({\n      type: BlockMovedMutationType,\n      detail: {\n        fromIndex: 1,\n        toIndex: 0,\n      },\n    }));\n  });\n\n  it('should be fired if something changed inside native input', () => {\n    createEditor([ {\n      type: 'code',\n      data: {\n        code: '',\n      },\n    } ]);\n\n    cy.get('[data-cy=editorjs')\n      .get('textarea')\n      .type('Some input to the textarea');\n\n    cy.get('@onChange').should('be.calledWithMatch', EditorJSApiMock, Cypress.sinon.match({\n      type: BlockChangedMutationType,\n      detail: {\n        index: 0,\n      },\n    }));\n  });\n\n  it('should not be fired on fake cursor adding and removing', () => {\n    createEditor([ {\n      type: 'paragraph',\n      data: {\n        text: 'some text',\n      },\n    } ]);\n\n    cy.get('[data-cy=editorjs')\n      .get('div.ce-block')\n      .click();\n\n    /**\n     * Open Block Tunes, add fake cursor\n     */\n    cy.get('[data-cy=editorjs]')\n      .get('span.ce-toolbar__settings-btn')\n      .click();\n\n    /**\n     * Close Block Tunes, remove fake cursor\n     */\n    cy.get('[data-cy=editorjs')\n      .get('div.ce-block')\n      .click();\n\n    cy.wait(modificationsObserverBatchTimeout).then(() => {\n      cy.get('@onChange').should('have.callCount', 0);\n    });\n  });\n\n  it('should be fired when the whole text inside block is removed', () => {\n    createEditor([ {\n      type: 'paragraph',\n      data: {\n        text: 'a',\n      },\n    } ]);\n\n    cy.get('[data-cy=editorjs')\n      .get('div.ce-block')\n      .click()\n      .type('{backspace}');\n\n    cy.get('@onChange').should('be.calledWithMatch', EditorJSApiMock, Cypress.sinon.match({\n      type: BlockChangedMutationType,\n      detail: {\n        index: 0,\n      },\n    }));\n  });\n\n  it('should not be fired when element with the \"data-mutation-free\" mark changes some attribute', () => {\n    /**\n     * Mock for tool wrapper which we will mutate in a test\n     */\n    const toolWrapper = document.createElement('div');\n\n    /**\n     * Mark it as mutation-free\n     */\n    toolWrapper.dataset.mutationFree = 'true';\n\n    /**\n     * Mock of tool with data-mutation-free attribute\n     */\n    class ToolWithMutationFreeAttribute {\n      /**\n       * Simply return mocked element\n       */\n      public render(): HTMLElement {\n        return toolWrapper;\n      }\n\n      /**\n       * Saving logic is not necessary for this test\n       */\n      // eslint-disable-next-line @typescript-eslint/no-empty-function\n      public save(): void {}\n    }\n\n    const editorConfig = {\n      tools: {\n        testTool: ToolWithMutationFreeAttribute,\n      },\n      onChange: (api, event): void => {\n        console.log('something changed', event);\n      },\n      data: {\n        blocks: [\n          {\n            type: 'testTool',\n            data: {},\n          },\n        ],\n      },\n    };\n\n    cy.spy(editorConfig, 'onChange').as('onChange');\n    cy.createEditor(editorConfig).as('editorInstance');\n\n    /**\n     * Emulate tool's internal attribute mutation\n     */\n    cy.wait(100).then(() => {\n      toolWrapper.setAttribute('some-changed-attr', 'some-new-value');\n    });\n\n    /**\n     * Check that onChange callback was not called\n     */\n    cy.wait(modificationsObserverBatchTimeout).then(() => {\n      cy.get('@onChange').should('have.callCount', 0);\n    });\n  });\n\n  it('should not be fired when mutation happened in a child of element with the \"data-mutation-free\" mark', () => {\n    /**\n     * Mock for tool wrapper which we will mutate in a test\n     */\n    const toolWrapper = document.createElement('div');\n    const toolChild = document.createElement('div');\n\n    toolWrapper.appendChild(toolChild);\n\n    /**\n     * Mark it as mutation-free\n     */\n    toolWrapper.dataset.mutationFree = 'true';\n\n    /**\n     * Mock of tool with data-mutation-free attribute\n     */\n    class ToolWithMutationFreeAttribute {\n      /**\n       * Simply return mocked element\n       */\n      public render(): HTMLElement {\n        return toolWrapper;\n      }\n\n      /**\n       * Saving logic is not necessary for this test\n       */\n      // eslint-disable-next-line @typescript-eslint/no-empty-function\n      public save(): void {}\n    }\n\n    const editorConfig = {\n      tools: {\n        testTool: ToolWithMutationFreeAttribute,\n      },\n      onChange: (api, event): void => {\n        console.log('something changed', event);\n      },\n      data: {\n        blocks: [\n          {\n            type: 'testTool',\n            data: {},\n          },\n        ],\n      },\n    };\n\n    cy.spy(editorConfig, 'onChange').as('onChange');\n    cy.createEditor(editorConfig).as('editorInstance');\n\n    /**\n     * Emulate tool's internal attribute mutation\n     */\n    cy.wait(100).then(() => {\n      toolChild.setAttribute('some-changed-attr', 'some-new-value');\n    });\n\n    /**\n     * Check that onChange callback was not called\n     */\n    cy.wait(modificationsObserverBatchTimeout).then(() => {\n      cy.get('@onChange').should('have.callCount', 0);\n    });\n  });\n\n  it('should not be fired when \"characterData\" mutation happened in a child of element with the \"data-mutation-free\" mark', () => {\n    /**\n     * Mock for tool wrapper which we will mutate in a test\n     */\n    const toolWrapper = document.createElement('div');\n    const toolChild = document.createElement('div');\n\n    toolChild.setAttribute('data-cy', 'tool-child');\n    toolChild.setAttribute('contenteditable', 'true');\n\n    toolWrapper.appendChild(toolChild);\n\n    /**\n     * Mark it as mutation-free\n     */\n    toolWrapper.dataset.mutationFree = 'true';\n\n    /**\n     * Mock of tool with data-mutation-free attribute\n     */\n    class ToolWithMutationFreeAttribute {\n      /**\n       * Simply return mocked element\n       */\n      public render(): HTMLElement {\n        return toolWrapper;\n      }\n\n      /**\n       * Saving logic is not necessary for this test\n       */\n      // eslint-disable-next-line @typescript-eslint/no-empty-function\n      public save(): void {}\n    }\n\n    const editorConfig = {\n      tools: {\n        testTool: ToolWithMutationFreeAttribute,\n      },\n      onChange: function (api, event) {\n        console.log('something changed!!!!!!!!', event);\n      },\n      data: {\n        blocks: [\n          {\n            type: 'testTool',\n            data: {},\n          },\n        ],\n      },\n    };\n\n    cy.spy(editorConfig, 'onChange').as('onChange');\n    cy.createEditor(editorConfig).as('editorInstance');\n\n    /**\n     * Emulate tool's child-element text typing\n     */\n    cy.get('[data-cy=editorjs')\n      .get('[data-cy=tool-child]')\n      .click()\n      .type('some text');\n\n    /**\n     * Check that onChange callback was not called\n     */\n    cy.wait(modificationsObserverBatchTimeout).then(() => {\n      cy.get('@onChange').should('have.callCount', 0);\n    });\n  });\n\n  it('should be called on blocks.clear() with removed and added blocks', () => {\n    createEditor([\n      {\n        type: 'paragraph',\n        data: {\n          text: 'The first paragraph',\n        },\n      },\n      {\n        type: 'paragraph',\n        data: {\n          text: 'The second paragraph',\n        },\n      },\n    ]);\n\n    cy.get<EditorJS>('@editorInstance')\n      .then(async editor => {\n        cy.wrap(editor.blocks.clear());\n      });\n\n    cy.get('@onChange').should('be.calledWithBatchedEvents', [\n      {\n        type: BlockRemovedMutationType,\n      },\n      {\n        type: BlockRemovedMutationType,\n      },\n      {\n        type: BlockAddedMutationType,\n      },\n    ]);\n  });\n\n  it('should not be called on blocks.render() on non-empty editor', () => {\n    createEditor([\n      {\n        type: 'paragraph',\n        data: {\n          text: 'The first paragraph',\n        },\n      },\n      {\n        type: 'paragraph',\n        data: {\n          text: 'The second paragraph',\n        },\n      },\n    ]);\n\n    cy.get<EditorJS>('@editorInstance')\n      .then(async editor => {\n        cy.wrap(editor.blocks.render({\n          blocks: [\n            {\n              type: 'paragraph',\n              data: {\n                text: 'The new paragraph',\n              },\n            },\n          ],\n        }));\n      });\n\n    cy.wait(modificationsObserverBatchTimeout);\n\n    cy.get('@onChange').should('have.callCount', 0);\n  });\n\n  it('should be called on blocks.update() with \"block-changed\" event', () => {\n    const block = {\n      id: 'bwnFX5LoX7',\n      type: 'paragraph',\n      data: {\n        text: 'The first block mock.',\n      },\n    };\n    const config = {\n      data: {\n        blocks: [\n          block,\n        ],\n      },\n      onChange: (api, event): void => {\n        console.log('something changed', event);\n      },\n    };\n\n    cy.spy(config, 'onChange').as('onChange');\n\n    cy.createEditor(config)\n      .then((editor) => {\n        editor.blocks.update(block.id, {\n          text: 'Updated text',\n        });\n\n        cy.get('@onChange').should('be.calledWithMatch', EditorJSApiMock, Cypress.sinon.match({\n          type: BlockChangedMutationType,\n          detail: {\n            index: 0,\n            target: {\n              id: block.id,\n            },\n          },\n        }));\n      });\n  });\n\n  it('should be fired when the whole text inside some descendant of the block is removed', () => {\n    /**\n     * Mock of Tool with nested contenteditable element\n     */\n    class ToolWithContentEditableDescendant extends ToolMock {\n      /**\n       * Creates element with nested contenteditable element\n       */\n      public render(): HTMLElement {\n        const contenteditable = document.createElement('div');\n\n        contenteditable.contentEditable = 'true';\n        contenteditable.innerText = 'a';\n        contenteditable.setAttribute('data-cy', 'nested-contenteditable');\n\n        const wrapper = document.createElement('div');\n\n        wrapper.appendChild(contenteditable);\n\n        return wrapper;\n      }\n    }\n\n    const config = {\n      tools: {\n        testTool: {\n          class: ToolWithContentEditableDescendant,\n        },\n      },\n      data: {\n        blocks: [\n          {\n            type: 'testTool',\n            data: 'a',\n          },\n        ],\n      },\n      onChange: (): void => {\n        console.log('something changed');\n      },\n    };\n\n    cy.spy(config, 'onChange').as('onChange');\n    cy.createEditor(config).as('editorInstance');\n\n    cy.get('[data-cy=nested-contenteditable]')\n      .click()\n      .clear();\n\n    cy.get('@onChange').should('be.calledWithMatch', EditorJSApiMock, Cypress.sinon.match({\n      type: BlockChangedMutationType,\n      detail: {\n        index: 0,\n      },\n    }));\n  });\n\n  it('should not be called when editor is initialized with readOnly mode', () => {\n    const config = {\n      readOnly: true,\n      onChange: (api, event): void => {\n        console.log('something changed', event);\n      },\n      data: {\n        blocks: [\n          {\n            type: 'paragraph',\n            data: {\n              text: 'The first paragraph',\n            },\n          },\n        ],\n      },\n    };\n\n    cy.spy(config, 'onChange').as('onChange');\n\n    cy.createEditor(config);\n\n    cy.wait(modificationsObserverBatchTimeout);\n\n    cy.get('@onChange').should('have.callCount', 0);\n  });\n\n  it('should not be called when editor is switched to/from readOnly mode', () => {\n    createEditor([\n      {\n        type: 'paragraph',\n        data: {\n          text: 'The first paragraph',\n        },\n      },\n    ]);\n\n    cy.get<EditorJS>('@editorInstance')\n      .then(async editor => {\n        editor.readOnly.toggle(true);\n      });\n\n    cy.wait(modificationsObserverBatchTimeout);\n\n    cy.get('@onChange').should('have.callCount', 0);\n\n    cy.get<EditorJS>('@editorInstance')\n      .then(async editor => {\n        editor.readOnly.toggle(false);\n      });\n\n    cy.wait(modificationsObserverBatchTimeout);\n\n    cy.get('@onChange').should('have.callCount', 0);\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/readOnly.cy.ts",
    "content": "import type { EditorConfig } from '../../../types';\nimport type EditorJS from '../../../types';\n\ndescribe('ReadOnly API spec', () => {\n  /**\n   * Creates the new editor instance\n   *\n   * @param config - Editor Config\n   */\n  function createEditor(config?: EditorConfig): void {\n    const editorConfig = Object.assign({}, config || {});\n\n    cy.createEditor(editorConfig).as('editorInstance');\n  }\n\n  it('should return correct value for readOnly.isEnabled when editor initialized in normal mode', () => {\n    createEditor();\n\n    cy\n      .get<EditorJS>('@editorInstance')\n      .then(editor => {\n        expect(editor.readOnly.isEnabled).to.be.false;\n      });\n  });\n\n  it('should return correct value for readOnly.isEnabled when editor initialized in read-only mode', () => {\n    createEditor({\n      readOnly: true,\n    });\n\n    cy\n      .get<EditorJS>('@editorInstance')\n      .then(editor => {\n        expect(editor.readOnly.isEnabled).to.be.true;\n      });\n  });\n\n  it('should return correct value for readOnly.isEnabled when read-only mode toggled', () => {\n    createEditor();\n\n    cy\n      .get<EditorJS>('@editorInstance')\n      .then(async editor => {\n        expect(editor.readOnly.isEnabled).to.be.false;\n\n        editor.readOnly.toggle()\n          .then(() => {\n            expect(editor.readOnly.isEnabled).to.be.true;\n          })\n          .then(() => editor.readOnly.toggle())\n          .then(() => {\n            expect(editor.readOnly.isEnabled).to.be.false;\n          });\n      });\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/sanitisation.cy.ts",
    "content": "import type EditorJS from '../../../types/index';\nimport type { OutputData } from '../../../types/index';\n\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\ndescribe('Sanitizing', () => {\n  context('Output should save inline formatting', () => {\n    it('should save initial formatting for paragraph', () => {\n      cy.createEditor({\n        data: {\n          blocks: [ {\n            type: 'paragraph',\n            data: { text: '<b>Bold text</b>' },\n          } ],\n        },\n      })\n        .then(async editor => {\n          cy.wrap<OutputData>(await editor.save())\n            .then((output) => {\n              const boldText = output.blocks[0].data.text;\n\n              expect(boldText).to.eq('<b>Bold text</b>');\n            });\n        });\n    });\n\n    it('should save formatting for paragraph', () => {\n      cy.createEditor({})\n        .as('editorInstance');\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-block')\n        .click()\n        .type('This text should be bold.{selectall}');\n\n      cy.get('[data-cy=editorjs]')\n        .get('[data-item-name=\"bold\"]')\n        .click();\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-block')\n        .click();\n\n      cy.get<EditorJS>('@editorInstance')\n        .then(async editorInstance => {\n          cy.wrap(await editorInstance.save())\n            .then((output) => {\n              const text = output.blocks[0].data.text;\n\n              expect(text).to.match(/<b>This text should be bold\\.(<br>)?<\\/b>/);\n            });\n        });\n    });\n\n    it('should save formatting for paragraph on paste', () => {\n      cy.createEditor({})\n        .as('editorInstance');\n\n      cy.get('[data-cy=editorjs]')\n        .get('div.ce-block')\n        .paste({\n          // eslint-disable-next-line @typescript-eslint/naming-convention\n          'text/html': '<p>Text</p><p><b>Bold text</b></p>',\n        });\n\n      cy.get<EditorJS>('@editorInstance')\n        .then(async editorInstance => {\n          cy.wrap<OutputData>(await editorInstance.save())\n            .then((output) => {\n              const boldText = output.blocks[1].data.text;\n\n              expect(boldText).to.eq('<b>Bold text</b>');\n            });\n        });\n    });\n  });\n\n  it('should sanitize unwanted html on blocks merging', function () {\n    cy.createEditor({\n      data: {\n        blocks: [\n          {\n            id: 'block1',\n            type: 'paragraph',\n            data: {\n              text: 'First block',\n            },\n          },\n          {\n            id: 'paragraph',\n            type: 'paragraph',\n            data: {\n              /**\n               * Tool does not support spans in its sanitization config\n               */\n              text: 'Second <span id=\"taint-html\">XSS<span> block',\n            },\n          },\n        ],\n      },\n    }).as('editorInstance');\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .last()\n      .click()\n      .type('{home}')\n      .type('{backspace}');\n\n    cy.get<EditorJS>('@editorInstance')\n      .then(async (editor) => {\n        const { blocks } = await editor.save();\n\n        expect(blocks[0].data.text).to.eq('First blockSecond XSS block'); // text has been merged, span has been removed\n      });\n  });\n});\n\n"
  },
  {
    "path": "test/cypress/tests/selection.cy.ts",
    "content": "import * as _ from '../../../src/components/utils';\n\ndescribe('Blocks selection', () => {\n  beforeEach(function () {\n    cy.createEditor({}).as('editorInstance');\n  });\n\n  afterEach(function () {\n    if (this.editorInstance) {\n      this.editorInstance.destroy();\n    }\n  });\n\n  it('should remove block selection on click', () => {\n    cy.get('[data-cy=editorjs]')\n      .find('div.ce-block')\n      .click()\n      .type('First block{enter}');\n\n    cy.get('[data-cy=editorjs')\n      .find('div.ce-block')\n      .next()\n      .type('Second block')\n      .type('{movetostart}')\n      .trigger('keydown', {\n        shiftKey: true,\n        keyCode: _.keyCodes.UP,\n      });\n\n    cy.get('[data-cy=editorjs')\n      .click()\n      .find('div.ce-block')\n      .should('not.have.class', '.ce-block--selected');\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/tools/BlockTool.cy.ts",
    "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\n/* tslint:disable:max-classes-per-file */\nimport type { BlockToolData, ToolSettings } from '@/types';\nimport { ToolType } from '@/types/tools/adapters/tool-type';\nimport BlockToolAdapter from '../../../../src/components/tools/block';\nimport InlineToolAdapter from '../../../../src/components/tools/inline';\nimport ToolsCollection from '../../../../src/components/tools/collection';\n\ndescribe('BlockTool', () => {\n  /**\n   * Mock for BlockTool constructor options\n   */\n  const options = {\n    name: 'blockTool',\n    constructable: class {\n      public static sanitize = {\n        rule1: {\n          div: true,\n        },\n      };\n\n      public static toolbox = {\n        icon: 'Tool icon',\n        title: 'Tool title',\n      };\n\n      public static enableLineBreaks = true;\n\n      public static pasteConfig = {\n        tags: [ 'div' ],\n      };\n\n      public static conversionConfig = {\n        import: 'import',\n        export: 'export',\n      };\n\n      public static isReadOnlySupported = true;\n\n      public static reset;\n      public static prepare;\n\n      public static shortcut = 'CTRL+N';\n\n      public data: BlockToolData;\n      public block: object;\n      public readonly: boolean;\n      public api: object;\n      public config: ToolSettings;\n\n      // eslint-disable-next-line jsdoc/require-jsdoc\n      constructor({ data, block, readOnly, api, config }) {\n        this.data = data;\n        this.block = block;\n        this.readonly = readOnly;\n        this.api = api;\n        this.config = config;\n      }\n    },\n    config: {\n      config: {\n        option1: 'option1',\n        option2: 'option2',\n      },\n      inlineToolbar: ['link', 'bold'],\n      tunes: ['anchor', 'favorites'],\n      shortcut: 'CMD+SHIFT+B',\n      toolbox: {\n        title: 'User Block Tool',\n        icon: 'User icon',\n      },\n    },\n    api: {\n      prop1: 'prop1',\n      prop2: 'prop2',\n    },\n    isDefault: false,\n    isInternal: false,\n    defaultPlaceholder: 'Default placeholder',\n  };\n\n  it('.type should return ToolType.Block', () => {\n    const tool = new BlockToolAdapter(options as any);\n\n    expect(tool.type).to.be.eq(ToolType.Block);\n  });\n\n  it('.name should return correct value', () => {\n    const tool = new BlockToolAdapter(options as any);\n\n    expect(tool.name).to.be.eq(options.name);\n  });\n\n  it('.isDefault should return correct value', () => {\n    const tool1 = new BlockToolAdapter(options as any);\n    const tool2 = new BlockToolAdapter({\n      ...options,\n      isDefault: true,\n    } as any);\n\n    expect(tool1.isDefault).to.be.false;\n    expect(tool2.isDefault).to.be.true;\n  });\n\n  it('.isInternal should return correct value', () => {\n    const tool1 = new BlockToolAdapter(options as any);\n    const tool2 = new BlockToolAdapter({\n      ...options,\n      isInternal: true,\n    } as any);\n\n    expect(tool1.isInternal).to.be.false;\n    expect(tool2.isInternal).to.be.true;\n  });\n\n  context('.settings', () => {\n    it('should return correct value', () => {\n      const tool = new BlockToolAdapter(options as any);\n\n      expect(tool.settings).to.be.deep.eq(options.config.config);\n    });\n\n    it('should add default placeholder if Tool is default', () => {\n      const tool = new BlockToolAdapter({\n        ...options,\n        isDefault: true,\n      } as any);\n\n      expect(tool.settings).to.have.property('placeholder').that.eq(options.defaultPlaceholder);\n    });\n  });\n\n  context('.sanitizeConfig', () => {\n    it('should return correct value', () => {\n      const tool = new BlockToolAdapter(options as any);\n\n      expect(tool.sanitizeConfig).to.be.deep.eq(options.constructable.sanitize);\n    });\n\n    it('should return composed config if there are enabled inline tools', () => {\n      const tool = new BlockToolAdapter(options as any);\n\n      const inlineTool = new InlineToolAdapter({\n        name: 'inlineTool',\n        constructable: class {\n          public static sanitize = {\n            b: true,\n          };\n        },\n        api: {},\n        config: {},\n      } as any);\n\n      tool.inlineTools = new ToolsCollection([ ['inlineTool', inlineTool] ]);\n\n      const expected = options.constructable.sanitize;\n\n      // tslint:disable-next-line:forin\n      for (const key in expected) {\n        expected[key] = {\n          ...expected[key],\n          b: true,\n        };\n      }\n\n      expect(tool.sanitizeConfig).to.be.deep.eq(expected);\n    });\n\n    it('should return inline tools config if block one is not set', () => {\n      const tool = new BlockToolAdapter({\n        ...options,\n        constructable: class {},\n      } as any);\n\n      const inlineTool1 = new InlineToolAdapter({\n        name: 'inlineTool',\n        constructable: class {\n          public static sanitize = {\n            b: true,\n          };\n        },\n        api: {},\n        config: {},\n      } as any);\n\n      const inlineTool2 = new InlineToolAdapter({\n        name: 'inlineTool',\n        constructable: class {\n          public static sanitize = {\n            a: true,\n          };\n        },\n        api: {},\n        config: {},\n      } as any);\n\n      tool.inlineTools = new ToolsCollection([ ['inlineTool', inlineTool1], ['inlineTool2', inlineTool2] ]);\n\n      expect(tool.sanitizeConfig).to.be.deep.eq(Object.assign(\n        {},\n        inlineTool1.sanitizeConfig,\n        inlineTool2.sanitizeConfig\n      ));\n    });\n\n    it('should return empty object by default', () => {\n      const tool = new BlockToolAdapter({\n        ...options,\n        constructable: class {},\n      } as any);\n\n      expect(tool.sanitizeConfig).to.be.deep.eq({});\n    });\n  });\n\n  it('.isBlock() should return true', () => {\n    const tool = new BlockToolAdapter(options as any);\n\n    expect(tool.isBlock()).to.be.true;\n  });\n\n  it('.isInline() should return false', () => {\n    const tool = new BlockToolAdapter(options as any);\n\n    expect(tool.isInline()).to.be.false;\n  });\n\n  it('.isTune() should return false', () => {\n    const tool = new BlockToolAdapter(options as any);\n\n    expect(tool.isTune()).to.be.false;\n  });\n\n  it('.isReadOnlySupported should return correct value', () => {\n    const tool = new BlockToolAdapter(options as any);\n\n    expect(tool.isReadOnlySupported).to.be.eq(options.constructable.isReadOnlySupported);\n  });\n\n  it('.isLineBreaksEnabled should return correct value', () => {\n    const tool = new BlockToolAdapter(options as any);\n\n    expect(tool.isLineBreaksEnabled).to.be.eq(options.constructable.enableLineBreaks);\n  });\n\n  it('.conversionConfig should return correct value', () => {\n    const tool = new BlockToolAdapter(options as any);\n\n    expect(tool.conversionConfig).to.be.deep.eq(options.constructable.conversionConfig);\n  });\n\n  describe('.pasteConfig', () => {\n    it('should return correct value', () => {\n      const tool = new BlockToolAdapter(options as any);\n\n      expect(tool.pasteConfig).to.be.deep.eq(options.constructable.pasteConfig);\n    });\n\n    it('should return false if `false` value was provided', () => {\n      const optionsWithDisabledPaste = {\n        ...options,\n        constructable: class extends (options.constructable as any) {\n          public static pasteConfig = false;\n        },\n      };\n      const tool = new BlockToolAdapter(optionsWithDisabledPaste as any);\n\n      expect(tool.pasteConfig).to.be.deep.eq(optionsWithDisabledPaste.constructable.pasteConfig);\n    });\n\n    it('should return empty object if getter isn\\'t provided', () => {\n      const optionsWithoutPasteConfig = {\n        ...options,\n        constructable: class extends (options.constructable as any) {\n          public static pasteConfig = undefined;\n        },\n      };\n      const tool = new BlockToolAdapter(optionsWithoutPasteConfig as any);\n\n      expect(tool.pasteConfig).to.be.deep.eq({});\n    });\n  });\n\n  context('.enabledInlineTools', () => {\n    it('should return correct value', () => {\n      const tool = new BlockToolAdapter(options as any);\n\n      expect(tool.enabledInlineTools).to.be.deep.eq(options.config.inlineToolbar);\n    });\n\n    it('should return false by default', () => {\n      const tool = new BlockToolAdapter({\n        ...options,\n        config: {\n          ...options.config,\n          inlineToolbar: undefined,\n        },\n      } as any);\n\n      expect(tool.enabledInlineTools).to.be.false;\n    });\n  });\n\n  it('.enabledBlockTunes should return correct value', () => {\n    const tool = new BlockToolAdapter(options as any);\n\n    expect(tool.enabledBlockTunes).to.be.deep.eq(options.config.tunes);\n  });\n\n  context('.prepare()', () => {\n    it('should call Tool prepare method', () => {\n      options.constructable.prepare = cy.stub();\n      const tool = new BlockToolAdapter(options as any);\n\n      tool.prepare();\n\n      expect(options.constructable.prepare).to.have.been.calledWithMatch({\n        toolName: tool.name,\n        config: tool.settings,\n      });\n    });\n\n    it('should not fail if Tool prepare method is not exist', () => {\n      const tool = new BlockToolAdapter({\n        ...options,\n        constructable: {},\n      } as any);\n\n      expect(tool.prepare).to.not.throw;\n    });\n  });\n\n  context('.reset()', () => {\n    it('should call Tool reset method', () => {\n      options.constructable.reset = cy.stub();\n      const tool = new BlockToolAdapter(options as any);\n\n      tool.reset();\n\n      expect(options.constructable.reset).to.be.calledOnce;\n    });\n\n    it('should not fail if Tool reset method is not exist', () => {\n      const tool = new BlockToolAdapter({\n        ...options,\n        constructable: {},\n      } as any);\n\n      expect(tool.reset).to.not.throw;\n    });\n  });\n\n  context('.shortcut', () => {\n    it('should return user provided shortcut', () => {\n      const tool = new BlockToolAdapter(options as any);\n\n      expect(tool.shortcut).to.be.eq(options.config.shortcut);\n    });\n\n    it('should return Tool provided shortcut if user one is not specified', () => {\n      const tool = new BlockToolAdapter({\n        ...options,\n        config: {\n          ...options.config,\n          shortcut: undefined,\n        },\n      } as any);\n\n      expect(tool.shortcut).to.be.eq(options.constructable.shortcut);\n    });\n  });\n\n  context('.toolbox', () => {\n    it('should return user provided toolbox config wrapped in array', () => {\n      const tool = new BlockToolAdapter(options as any);\n\n      expect(tool.toolbox).to.be.deep.eq([ options.config.toolbox ]);\n    });\n\n    it('should return Tool provided toolbox config wrapped in array if user one is not specified', () => {\n      const tool = new BlockToolAdapter({\n        ...options,\n        config: {\n          ...options.config,\n          toolbox: undefined,\n        },\n      } as any);\n\n      expect(tool.toolbox).to.be.deep.eq([ options.constructable.toolbox ]);\n    });\n\n    it('should merge Tool provided toolbox config and user one and wrap result in array in case both are objects', () => {\n      const tool1 = new BlockToolAdapter({\n        ...options,\n        config: {\n          ...options.config,\n          toolbox: {\n            title: options.config.toolbox.title,\n          },\n        },\n      } as any);\n      const tool2 = new BlockToolAdapter({\n        ...options,\n        config: {\n          ...options.config,\n          toolbox: {\n            icon: options.config.toolbox.icon,\n          },\n        },\n      } as any);\n\n      expect(tool1.toolbox).to.be.deep.eq([ Object.assign({}, options.constructable.toolbox, { title: options.config.toolbox.title }) ]);\n      expect(tool2.toolbox).to.be.deep.eq([ Object.assign({}, options.constructable.toolbox, { icon: options.config.toolbox.icon }) ]);\n    });\n\n    it('should replace Tool provided toolbox config with user defined config in case the first is an array and the second is an object', () => {\n      const toolboxEntries = [\n        {\n          title: 'Toolbox entry 1',\n        },\n        {\n          title: 'Toolbox entry 2',\n        },\n      ];\n      const userDefinedToolboxConfig = {\n        icon: options.config.toolbox.icon,\n        title: options.config.toolbox.title,\n      };\n      const tool = new BlockToolAdapter({\n        ...options,\n        constructable: {\n          ...options.constructable,\n          toolbox: toolboxEntries,\n        },\n        config: {\n          ...options.config,\n          toolbox: userDefinedToolboxConfig,\n        },\n      } as any);\n\n      expect(tool.toolbox).to.be.deep.eq([ userDefinedToolboxConfig ]);\n    });\n\n    it('should replace Tool provided toolbox config with user defined config in case the first is an object and the second is an array', () => {\n      const userDefinedToolboxConfig = [\n        {\n          title: 'Toolbox entry 1',\n        },\n        {\n          title: 'Toolbox entry 2',\n        },\n      ];\n      const tool = new BlockToolAdapter({\n        ...options,\n        config: {\n          ...options.config,\n          toolbox: userDefinedToolboxConfig,\n        },\n      } as any);\n\n      expect(tool.toolbox).to.be.deep.eq(userDefinedToolboxConfig);\n    });\n\n    it('should merge Tool provided toolbox config with user defined config in case both are arrays', () => {\n      const toolboxEntries = [\n        {\n          title: 'Toolbox entry 1',\n        },\n      ];\n\n      const userDefinedToolboxConfig = [\n        {\n          icon: 'Icon 1',\n        },\n        {\n          icon: 'Icon 2',\n          title: 'Toolbox entry 2',\n        },\n      ];\n\n      const tool = new BlockToolAdapter({\n        ...options,\n        constructable: {\n          ...options.constructable,\n          toolbox: toolboxEntries,\n        },\n        config: {\n          ...options.config,\n          toolbox: userDefinedToolboxConfig,\n        },\n      } as any);\n\n      const expected = userDefinedToolboxConfig.map((item, i) => {\n        const toolToolboxEntry = toolboxEntries[i];\n\n        if (toolToolboxEntry) {\n          return {\n            ...toolToolboxEntry,\n            ...item,\n          };\n        }\n\n        return item;\n      });\n\n      expect(tool.toolbox).to.be.deep.eq(expected);\n    });\n\n    it('should return undefined if user specifies false as a value', () => {\n      const tool = new BlockToolAdapter({\n        ...options,\n        config: {\n          ...options.config,\n          toolbox: false,\n        },\n      } as any);\n\n      expect(tool.toolbox).to.be.undefined;\n    });\n\n    it('should return undefined if Tool specifies false as a value', () => {\n      const tool = new BlockToolAdapter({\n        ...options,\n        constructable: class {\n          public static toolbox = false;\n        },\n      } as any);\n\n      expect(tool.toolbox).to.be.undefined;\n    });\n\n    it('should return undefined if Tool provides empty config', () => {\n      const tool = new BlockToolAdapter({\n        ...options,\n        constructable: class {\n          public static toolbox = {};\n        },\n      } as any);\n\n      expect(tool.toolbox).to.be.undefined;\n    });\n  });\n\n  context('.create()', () => {\n    const tool = new BlockToolAdapter(options as any);\n    const data = { text: 'text' };\n    const blockAPI = {\n      // eslint-disable-next-line @typescript-eslint/no-empty-function\n      method(): void {},\n    };\n\n    it('should return Tool instance', () => {\n      expect(tool.create(data, blockAPI as any, false)).to.be.instanceOf(options.constructable);\n    });\n\n    it('should return Tool instance with passed data', () => {\n      const instance = tool.create(data, blockAPI as any, false) as any;\n\n      expect(instance.data).to.be.deep.eq(data);\n    });\n\n    it('should return Tool instance with passed BlockAPI object', () => {\n      const instance = tool.create(data, blockAPI as any, false) as any;\n\n      expect(instance.block).to.be.deep.eq(blockAPI);\n    });\n\n    it('should return Tool instance with passed readOnly flag', () => {\n      const instance1 = tool.create(data, blockAPI as any, false) as any;\n      const instance2 = tool.create(data, blockAPI as any, true) as any;\n\n      expect(instance1.readonly).to.be.eq(false);\n      expect(instance2.readonly).to.be.eq(true);\n    });\n\n    it('should return Tool instance with passed API object', () => {\n      const instance = tool.create(data, blockAPI as any, false) as any;\n\n      expect(instance.api).to.be.deep.eq(options.api);\n    });\n\n    it('should return Tool instance with passed config', () => {\n      const instance = tool.create(data, blockAPI as any, false) as any;\n\n      expect(instance.config).to.be.deep.eq(options.config.config);\n    });\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/tools/BlockTune.cy.ts",
    "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\n/* tslint:disable:max-classes-per-file */\nimport type { ToolSettings } from '@/types';\nimport { ToolType } from '@/types/tools/adapters/tool-type';\nimport BlockTuneAdapter from '../../../../src/components/tools/tune';\nimport type { BlockTuneData } from '@/types/block-tunes/block-tune-data';\n\ndescribe('BlockTune', () => {\n  /**\n   * Mock for BlockTune constructor options\n   */\n  const options = {\n    name: 'blockTune',\n    constructable: class {\n      public static reset;\n      public static prepare;\n\n      public api: object;\n      public config: ToolSettings;\n      public data: BlockTuneData;\n      public block: object;\n\n      // eslint-disable-next-line jsdoc/require-jsdoc\n      constructor({ api, config, block, data }) {\n        this.api = api;\n        this.config = config;\n        this.block = block;\n        this.data = data;\n      }\n    },\n    config: {\n      config: {\n        option1: 'option1',\n        option2: 'option2',\n      },\n      shortcut: 'CMD+SHIFT+B',\n    },\n    api: {\n      prop1: 'prop1',\n      prop2: 'prop2',\n    },\n    isDefault: false,\n    isInternal: false,\n    defaultPlaceholder: 'Default placeholder',\n  };\n\n  it('.type should return ToolType.Tune', () => {\n    const tool = new BlockTuneAdapter(options as any);\n\n    expect(tool.type).to.be.eq(ToolType.Tune);\n  });\n\n  it('.name should return correct value', () => {\n    const tool = new BlockTuneAdapter(options as any);\n\n    expect(tool.name).to.be.eq(options.name);\n  });\n\n  it('.isInternal should return correct value', () => {\n    const tool1 = new BlockTuneAdapter(options as any);\n    const tool2 = new BlockTuneAdapter({\n      ...options,\n      isInternal: true,\n    } as any);\n\n    expect(tool1.isInternal).to.be.false;\n    expect(tool2.isInternal).to.be.true;\n  });\n\n  it('.settings should return correct value', () => {\n    const tool = new BlockTuneAdapter(options as any);\n\n    expect(tool.settings).to.be.deep.eq(options.config.config);\n  });\n\n  it('.isBlock() should return false', () => {\n    const tool = new BlockTuneAdapter(options as any);\n\n    expect(tool.isBlock()).to.be.false;\n  });\n\n  it('.isInline() should return false', () => {\n    const tool = new BlockTuneAdapter(options as any);\n\n    expect(tool.isInline()).to.be.false;\n  });\n\n  it('.isTune() should return true', () => {\n    const tool = new BlockTuneAdapter(options as any);\n\n    expect(tool.isTune()).to.be.true;\n  });\n\n  context('.prepare()', () => {\n    it('should call Tool prepare method', () => {\n      options.constructable.prepare = cy.stub();\n      const tool = new BlockTuneAdapter(options as any);\n\n      tool.prepare();\n\n      expect(options.constructable.prepare).to.have.been.calledWithMatch({\n        toolName: tool.name,\n        config: tool.settings,\n      });\n    });\n\n    it('should not fail if Tool prepare method is not exist', () => {\n      const tool = new BlockTuneAdapter({\n        ...options,\n        constructable: {},\n      } as any);\n\n      expect(tool.prepare).to.not.throw;\n    });\n  });\n\n  context('.reset()', () => {\n    it('should call Tool reset method', () => {\n      options.constructable.reset = cy.stub();\n      const tool = new BlockTuneAdapter(options as any);\n\n      tool.reset();\n\n      expect(options.constructable.reset).to.be.calledOnce;\n    });\n\n    it('should not fail if Tool reset method is not exist', () => {\n      const tool = new BlockTuneAdapter({\n        ...options,\n        constructable: {},\n      } as any);\n\n      expect(tool.reset).to.not.throw;\n    });\n  });\n\n  context('.create()', () => {\n    const tool = new BlockTuneAdapter(options as any);\n    const data = { text: 'text' };\n    const blockAPI = {\n      // eslint-disable-next-line @typescript-eslint/no-empty-function\n      method(): void {},\n    };\n\n    it('should return Tool instance', () => {\n      expect(tool.create(data, blockAPI as any)).to.be.instanceOf(options.constructable);\n    });\n\n    it('should return Tool instance with passed data', () => {\n      const instance = tool.create(data, blockAPI as any) as any;\n\n      expect(instance.data).to.be.deep.eq(data);\n    });\n\n    it('should return Tool instance with passed BlockAPI object', () => {\n      const instance = tool.create(data, blockAPI as any) as any;\n\n      expect(instance.block).to.be.deep.eq(blockAPI);\n    });\n\n    it('should return Tool instance with passed API object', () => {\n      const instance = tool.create(data, blockAPI as any) as any;\n\n      expect(instance.api).to.be.deep.eq(options.api);\n    });\n\n    it('should return Tool instance with passed settings', () => {\n      const instance = tool.create(data, blockAPI as any) as any;\n\n      expect(instance.config).to.be.deep.eq(options.config.config);\n    });\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/tools/InlineTool.cy.ts",
    "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\n/* tslint:disable:max-classes-per-file */\nimport type { ToolSettings } from '@/types';\nimport { ToolType } from '@/types/tools/adapters/tool-type';\nimport InlineToolAdapter from '../../../../src/components/tools/inline';\n\ndescribe('InlineTool', () => {\n  /**\n   * Mock for InlineTool constructor options\n   */\n  const options = {\n    name: 'inlineTool',\n    constructable: class {\n      public static sanitize = {\n        rule1: 'rule1',\n      };\n\n      public static title = 'Title';\n\n      public static reset;\n      public static prepare;\n\n      public static shortcut = 'CTRL+N';\n      public static isReadOnlySupported = true;\n\n      public api: object;\n      public config: ToolSettings;\n\n      /**\n       * @param options - constructor options\n       * @param options.api - EditorAPI\n       * @param options.config - tool config\n       */\n      constructor({ api, config }) {\n        this.api = api;\n        this.config = config;\n      }\n    },\n    config: {\n      config: {\n        option1: 'option1',\n        option2: 'option2',\n      },\n      shortcut: 'CMD+SHIFT+B',\n    },\n    api: {\n      prop1: 'prop1',\n      prop2: 'prop2',\n    },\n    isDefault: false,\n    isInternal: false,\n    defaultPlaceholder: 'Default placeholder',\n  };\n\n  it('.type should return ToolType.Inline', () => {\n    const tool = new InlineToolAdapter(options as any);\n\n    expect(tool.type).to.be.eq(ToolType.Inline);\n  });\n\n  it('.name should return correct value', () => {\n    const tool = new InlineToolAdapter(options as any);\n\n    expect(tool.name).to.be.eq(options.name);\n  });\n\n  it('.title should return correct title', () => {\n    const tool = new InlineToolAdapter(options as any);\n\n    expect(tool.title).to.be.eq(options.constructable.title);\n  });\n\n  it('.isInternal should return correct value', () => {\n    const tool1 = new InlineToolAdapter(options as any);\n    const tool2 = new InlineToolAdapter({\n      ...options,\n      isInternal: true,\n    } as any);\n\n    expect(tool1.isInternal).to.be.false;\n    expect(tool2.isInternal).to.be.true;\n  });\n\n  it('.settings should return correct value', () => {\n    const tool = new InlineToolAdapter(options as any);\n\n    expect(tool.settings).to.be.deep.eq(options.config.config);\n  });\n\n  it('.sanitizeConfig should return correct value', () => {\n    const tool = new InlineToolAdapter(options as any);\n\n    expect(tool.sanitizeConfig).to.be.deep.eq(options.constructable.sanitize);\n  });\n\n  it('.isBlock() should return false', () => {\n    const tool = new InlineToolAdapter(options as any);\n\n    expect(tool.isBlock()).to.be.false;\n  });\n\n  it('.isInline() should return true', () => {\n    const tool = new InlineToolAdapter(options as any);\n\n    expect(tool.isInline()).to.be.true;\n  });\n\n  it('.isTune() should return false', () => {\n    const tool = new InlineToolAdapter(options as any);\n\n    expect(tool.isTune()).to.be.false;\n  });\n\n  context('.prepare()', () => {\n    it('should call Tool prepare method', () => {\n      options.constructable.prepare = cy.stub();\n      const tool = new InlineToolAdapter(options as any);\n\n      tool.prepare();\n\n      expect(options.constructable.prepare).to.have.been.calledWithMatch({\n        toolName: tool.name,\n        config: tool.settings,\n      });\n    });\n\n    it('should not fail if Tool prepare method is not exist', () => {\n      const tool = new InlineToolAdapter({\n        ...options,\n        constructable: {},\n      } as any);\n\n      expect(tool.prepare).to.not.throw;\n    });\n  });\n\n  context('.reset()', () => {\n    it('should call Tool reset method', () => {\n      options.constructable.reset = cy.stub();\n      const tool = new InlineToolAdapter(options as any);\n\n      tool.reset();\n\n      expect(options.constructable.reset).to.be.calledOnce;\n    });\n\n    it('should not fail if Tool reset method is not exist', () => {\n      const tool = new InlineToolAdapter({\n        ...options,\n        constructable: {},\n      } as any);\n\n      expect(tool.reset).to.not.throw;\n    });\n  });\n\n  context('.shortcut', () => {\n    it('should return user provided shortcut', () => {\n      const tool = new InlineToolAdapter(options as any);\n\n      expect(tool.shortcut).to.be.eq(options.config.shortcut);\n    });\n\n    it('should return Tool provided shortcut if user one is not specified', () => {\n      const tool = new InlineToolAdapter({\n        ...options,\n        config: {\n          ...options.config,\n          shortcut: undefined,\n        },\n      } as any);\n\n      expect(tool.shortcut).to.be.eq(options.constructable.shortcut);\n    });\n  });\n\n  context('.create()', () => {\n    const tool = new InlineToolAdapter(options as any);\n\n    it('should return Tool instance', () => {\n      expect(tool.create()).to.be.instanceOf(options.constructable);\n    });\n\n    it('should return Tool instance with passed API object', () => {\n      const instance = tool.create() as any;\n\n      expect(instance.api).to.be.deep.eq(options.api);\n    });\n\n    it('should return Tool instance with passed config', () => {\n      const instance = tool.create() as any;\n\n      expect(instance.config).to.be.deep.eq(options.config.config);\n    });\n  });\n\n  context('.isReadOnlySupported', () => {\n    it('should return Tool provided value', () => {\n      const tool = new InlineToolAdapter(options as any);\n\n      expect(tool.isReadOnlySupported).to.be.eq(options.constructable.isReadOnlySupported);\n    });\n\n    it('should return false if Tool provided value is not exist', () => {\n      const tool = new InlineToolAdapter({\n        ...options,\n        constructable: {},\n      } as any);\n\n      expect(tool.isReadOnlySupported).to.be.false;\n    });\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/tools/ToolsCollection.cy.ts",
    "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport ToolsCollection from '../../../../src/components/tools/collection';\nimport type BlockToolAdapter from '../../../../src/components/tools/block';\nimport type InlineToolAdapter from '../../../../src/components/tools/inline';\nimport type BlockTuneAdapter from '../../../../src/components/tools/tune';\nimport type BaseToolAdapter from '../../../../src/components/tools/base';\n\nconst FakeTool = {\n  isBlock(): boolean {\n    return false;\n  },\n  isInline(): boolean {\n    return false;\n  },\n  isTune(): boolean {\n    return false;\n  },\n  isInternal: false,\n};\n\nconst FakeBlockTool = {\n  ...FakeTool,\n  isBlock(): boolean {\n    return true;\n  },\n};\n\nconst FakeInlineTool = {\n  ...FakeTool,\n  isInline(): boolean {\n    return true;\n  },\n};\n\nconst FakeBlockTune = {\n  ...FakeTool,\n  isTune(): boolean {\n    return true;\n  },\n};\n\n/**\n * Unit tests for ToolsCollection class\n */\ndescribe('ToolsCollection', (): void => {\n  let collection;\n\n  /**\n   * Mock for Tools in collection\n   */\n  const fakeTools = [\n    ['block1', FakeBlockTool],\n    ['inline1', FakeInlineTool],\n    ['block2', {\n      ...FakeBlockTool,\n      isInternal: true,\n    } ],\n    ['tune1', FakeBlockTune],\n    ['block3', FakeBlockTool],\n    ['inline2', {\n      ...FakeInlineTool,\n      isInternal: true,\n    } ],\n    ['tune2', FakeBlockTune],\n    ['tune3', {\n      ...FakeBlockTune,\n      isInternal: true,\n    } ],\n    ['block3', FakeInlineTool],\n    ['block4', FakeBlockTool],\n  ];\n\n  beforeEach((): void => {\n    collection = new ToolsCollection(fakeTools as any);\n  });\n\n  it('should be instance of Map', (): void => {\n    expect(collection instanceof Map).to.be.true;\n  });\n\n  context('.blockTools', (): void => {\n    it('should return new instance of ToolsCollection', (): void => {\n      expect(collection.blockTools instanceof ToolsCollection).to.be.true;\n    });\n\n    it('result should contain only block tools', (): void => {\n      expect(\n        Array\n          .from(\n            collection.blockTools.values()\n          )\n          .every((tool: BlockToolAdapter) => tool.isBlock())\n      ).to.be.true;\n    });\n  });\n\n  context('.inlineTools', (): void => {\n    it('should return new instance of ToolsCollection', (): void => {\n      expect(collection.inlineTools instanceof ToolsCollection).to.be.true;\n    });\n\n    it('result should contain only inline tools', (): void => {\n      expect(\n        Array\n          .from(\n            collection.inlineTools.values()\n          )\n          .every((tool: InlineToolAdapter) => tool.isInline())\n      ).to.be.true;\n    });\n  });\n\n  context('.blockTunes', (): void => {\n    it('should return new instance of ToolsCollection', (): void => {\n      expect(collection.blockTunes instanceof ToolsCollection).to.be.true;\n    });\n\n    it('result should contain only block tools', (): void => {\n      expect(\n        Array\n          .from(\n            collection.blockTunes.values()\n          )\n          .every((tool: BlockTuneAdapter) => tool.isTune())\n      ).to.be.true;\n    });\n  });\n\n  context('.internalTools', (): void => {\n    it('should return new instance of ToolsCollection', (): void => {\n      expect(collection.internalTools instanceof ToolsCollection).to.be.true;\n    });\n\n    it('result should contain only internal tools', (): void => {\n      expect(\n        Array\n          .from(\n            collection.internalTools.values()\n          )\n          .every((tool: BaseToolAdapter) => tool.isInternal)\n      ).to.be.true;\n    });\n  });\n\n  context('.externalTools', (): void => {\n    it('should return new instance of ToolsCollection', (): void => {\n      expect(collection.externalTools instanceof ToolsCollection).to.be.true;\n    });\n\n    it('result should contain only external tools', (): void => {\n      expect(\n        Array\n          .from(\n            collection.externalTools.values()\n          )\n          .every((tool: BaseToolAdapter) => !tool.isInternal)\n      ).to.be.true;\n    });\n  });\n\n  context('mixed access', (): void => {\n    context('.blockTunes.internalTools', (): void => {\n      it('should return only internal tunes', (): void => {\n        expect(\n          Array\n            .from(\n              collection.blockTunes.internalTools.values()\n            )\n            .every((tool: BlockTuneAdapter) => tool.isTune() && tool.isInternal)\n        ).to.be.true;\n      });\n    });\n\n    context('.externalTools.blockTools', (): void => {\n      it('should return only external block tools', (): void => {\n        expect(\n          Array\n            .from(\n              collection.externalTools.blockTools.values()\n            )\n            .every((tool: BlockToolAdapter) => tool.isBlock() && !tool.isInternal)\n        ).to.be.true;\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/tools/ToolsFactory.cy.ts",
    "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport LinkInlineTool from '../../../../src/components/inline-tools/inline-tool-link';\nimport MoveUpTune from '../../../../src/components/block-tunes/block-tune-move-up';\nimport ToolsFactory from '../../../../src/components/tools/factory';\nimport InlineToolAdapter from '../../../../src/components/tools/inline';\nimport BlockToolAdapter from '../../../../src/components/tools/block';\nimport BlockTuneAdapter from '../../../../src/components/tools/tune';\nimport Paragraph from '@editorjs/paragraph';\n\ndescribe('ToolsFactory', (): void => {\n  let factory;\n  const config = {\n    paragraph: {\n      class: Paragraph,\n    },\n    link: {\n      class: LinkInlineTool,\n    },\n    moveUp: {\n      class: MoveUpTune,\n    },\n  };\n\n  beforeEach((): void => {\n    factory = new ToolsFactory(\n      config,\n      {\n        placeholder: 'Placeholder',\n        defaultBlock: 'paragraph',\n      } as any,\n      {\n        getMethodsForTool(): object {\n          return {\n            prop1: 'prop1',\n            prop2: 'prop2',\n          };\n        },\n      } as any\n    );\n  });\n\n  context('.get', (): void => {\n    it('should return appropriate tool object', (): void => {\n      const tool = factory.get('link');\n\n      expect(tool.name).to.be.eq('link');\n    });\n\n    it('should return InlineTool object for inline tool', (): void => {\n      const tool = factory.get('link');\n\n      expect(tool instanceof InlineToolAdapter).to.be.true;\n    });\n\n    it('should return BlockTool object for block tool', (): void => {\n      const tool = factory.get('paragraph');\n\n      expect(tool instanceof BlockToolAdapter).to.be.true;\n    });\n\n    it('should return BlockTune object for tune', (): void => {\n      const tool = factory.get('moveUp');\n\n      expect(tool instanceof BlockTuneAdapter).to.be.true;\n    });\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/ui/BlockTunes.cy.ts",
    "content": "import { selectionChangeDebounceTimeout } from '../../../../src/components/constants';\nimport Header from '@editorjs/header';\nimport type { ConversionConfig, ToolboxConfig } from '../../../../types';\nimport type { MenuConfig } from '../../../../types/tools';\nimport { ToolWithoutConversionExport } from '../../fixtures/tools/ToolWithoutConversionExport';\n\ndescribe('BlockTunes', function () {\n  describe('Search', () => {\n    it('should be focused after popover opened', () => {\n      cy.createEditor({\n        data: {\n          blocks: [\n            {\n              type: 'paragraph',\n              data: {\n                text: 'Some text',\n              },\n            },\n          ],\n        },\n      });\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .click()\n        .type('{cmd}/')\n        .wait(selectionChangeDebounceTimeout);\n\n      /**\n       * Caret is set to the search input\n       */\n      cy.window()\n        .then((window) => {\n          const selection = window.getSelection();\n\n          expect(selection.rangeCount).to.be.equal(1);\n\n          const range = selection.getRangeAt(0);\n\n          cy.get('[data-cy=editorjs]')\n            .find('[data-cy=\"block-tunes\"] .cdx-search-field')\n            .should(($block) => {\n              expect($block[0].contains(range.startContainer)).to.be.true;\n            });\n        });\n    });\n  });\n\n  describe('Keyboard only', function () {\n    it('should not delete the currently selected block when Enter pressed on a search input (or any block tune)', function () {\n      const ENTER_KEY_CODE = 13;\n\n      cy.createEditor({\n        data: {\n          blocks: [\n            {\n              type: 'paragraph',\n              data: {\n                text: 'Some text',\n              },\n            },\n          ],\n        },\n      });\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .click()\n        .type('{cmd}/')\n        .wait(selectionChangeDebounceTimeout)\n        .keydown(ENTER_KEY_CODE);\n\n      /**\n       * Block should have same text\n       */\n      cy.get('[data-cy=\"block-wrapper\"')\n        .should('have.text', 'Some text');\n    });\n\n    it('should not unselect currently selected block when Enter pressed on a block tune', function () {\n      const ENTER_KEY_CODE = 13;\n\n      cy.createEditor({\n        data: {\n          blocks: [\n            {\n              type: 'paragraph',\n              data: {\n                text: 'Some text',\n              },\n            },\n          ],\n        },\n      });\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .click()\n        .type('{cmd}/')\n        .wait(selectionChangeDebounceTimeout)\n        .keydown(ENTER_KEY_CODE);\n\n      /**\n       * Block should not be selected\n       */\n      cy.get('[data-cy=\"block-wrapper\"')\n        .first()\n        .should('have.class', 'ce-block--selected');\n    });\n  });\n\n  describe('Convert to', () => {\n    it('should display Convert to inside Block Tunes', () => {\n      cy.createEditor({\n        tools: {\n          header: Header,\n        },\n        data: {\n          blocks: [\n            {\n              type: 'paragraph',\n              data: {\n                text: 'Some text',\n              },\n            },\n          ],\n        },\n      });\n\n      /** Open block tunes menu */\n      cy.get('[data-cy=editorjs]')\n        .get('.cdx-block')\n        .click();\n\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-toolbar__settings-btn')\n        .click();\n\n      /** Check \"Convert to\" option is present  */\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-popover-item')\n        .contains('Convert to')\n        .should('exist');\n\n      /** Click \"Convert to\" option*/\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-popover-item')\n        .contains('Convert to')\n        .click();\n\n      /** Check nected popover with \"Heading\" option is present */\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-popover--nested [data-item-name=header]')\n        .should('exist');\n    });\n\n    it('should not display Convert to inside Block Tunes if there is nothing to convert to', () => {\n      /** Editor instance with single default tool */\n      cy.createEditor({\n        data: {\n          blocks: [\n            {\n              type: 'paragraph',\n              data: {\n                text: 'Some text',\n              },\n            },\n          ],\n        },\n      });\n\n      /** Open block tunes menu */\n      cy.get('[data-cy=editorjs]')\n        .get('.cdx-block')\n        .click();\n\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-toolbar__settings-btn')\n        .click();\n\n      /** Check \"Convert to\" option is not present  */\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-popover-item')\n        .contains('Convert to')\n        .should('not.exist');\n    });\n\n    it('should not display the ConvertTo control if block has no conversionConfig.export specified', () => {\n      cy.createEditor({\n        tools: {\n          testTool: ToolWithoutConversionExport,\n        },\n        data: {\n          blocks: [\n            {\n              type: 'testTool',\n              data: {\n                text: 'Some text',\n              },\n            },\n          ],\n        },\n      }).as('editorInstance');\n\n      cy.get('@editorInstance')\n        .get('[data-cy=editorjs]')\n        .find('.ce-block')\n        .click();\n\n      cy.get('@editorInstance')\n        .get('[data-cy=editorjs]')\n        .find('.ce-toolbar__settings-btn')\n        .click();\n\n      cy.get('@editorInstance')\n        .get('[data-cy=editorjs]')\n        .find('.ce-popover-item[data-item-name=convert-to]')\n        .should('not.exist');\n    });\n\n    it('should not display tool with the same data in \"Convert to\" menu', () => {\n      /**\n       * Tool with several toolbox entries configured\n       */\n      class TestTool {\n        /**\n         * Tool is convertable\n         */\n        public static get conversionConfig(): ConversionConfig {\n          return {\n            import: 'text',\n            export: 'text',\n          };\n        }\n\n        /**\n         * TestTool contains several toolbox options\n         */\n        public static get toolbox(): ToolboxConfig {\n          return [\n            {\n              title: 'Title 1',\n              icon: 'Icon1',\n              data: {\n                level: 1,\n              },\n            },\n            {\n              title: 'Title 2',\n              icon: 'Icon2',\n              data: {\n                level: 2,\n              },\n            },\n          ];\n        }\n\n        /**\n         * Tool can render itself\n         */\n        public render(): HTMLDivElement {\n          const div = document.createElement('div');\n\n          div.innerText = 'Some text';\n\n          return div;\n        }\n\n        /**\n         * Tool can save it's data\n         */\n        public save(): { text: string; level: number } {\n          return {\n            text: 'Some text',\n            level: 1,\n          };\n        }\n      }\n\n      /** Editor instance with TestTool installed and one block of TestTool type */\n      cy.createEditor({\n        tools: {\n          testTool: TestTool,\n        },\n        data: {\n          blocks: [\n            {\n              type: 'testTool',\n              data: {\n                text: 'Some text',\n                level: 1,\n              },\n            },\n          ],\n        },\n      });\n\n      /** Open block tunes menu */\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-block')\n        .click();\n\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-toolbar__settings-btn')\n        .click();\n\n      /** Open \"Convert to\" menu  */\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-popover-item')\n        .contains('Convert to')\n        .click();\n\n      /** Check TestTool option with SAME data is NOT present */\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-popover--nested [data-item-name=testTool]')\n        .contains('Title 1')\n        .should('not.exist');\n\n      /** Check TestTool option with DIFFERENT data IS present */\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-popover--nested [data-item-name=testTool]')\n        .contains('Title 2')\n        .should('exist');\n    });\n\n    it('should convert block to another type and set caret to the new block', () => {\n      cy.createEditor({\n        tools: {\n          header: Header,\n        },\n        data: {\n          blocks: [\n            {\n              type: 'paragraph',\n              data: {\n                text: 'Some text',\n              },\n            },\n          ],\n        },\n      });\n\n      /** Open block tunes menu */\n      cy.get('[data-cy=editorjs]')\n        .get('.cdx-block')\n        .click();\n\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-toolbar__settings-btn')\n        .click();\n\n      /** Click \"Convert to\" option*/\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-popover-item')\n        .contains('Convert to')\n        .click();\n\n      /** Click \"Heading\" option */\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-popover--nested [data-item-name=header]')\n        .click();\n\n      /** Check the block was converted to the second option */\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-header')\n        .should('have.text', 'Some text');\n\n      /** Check that caret set to the end of the new block */\n      cy.window()\n        .then((window) => {\n          const selection = window.getSelection();\n          const range = selection.getRangeAt(0);\n\n          cy.get('[data-cy=editorjs]')\n            .find('.ce-header')\n            .should(($block) => {\n              expect($block[0].contains(range.startContainer)).to.be.true;\n            });\n        });\n    });\n  });\n\n  describe('Tunes order', () => {\n    it('should display block specific tunes before common tunes', () => {\n      /**\n       * Tool with several toolbox entries configured\n       */\n      class TestTool {\n        /**\n         * TestTool contains several toolbox options\n         */\n        public static get toolbox(): ToolboxConfig {\n          return [\n            {\n              title: 'Title 1',\n              icon: 'Icon1',\n              data: {\n                level: 1,\n              },\n            },\n          ];\n        }\n\n        /**\n         * Tool can render itself\n         */\n        public render(): HTMLDivElement {\n          const div = document.createElement('div');\n\n          div.innerText = 'Some text';\n\n          return div;\n        }\n\n        /**\n         *\n         */\n        public renderSettings(): MenuConfig {\n          return {\n            icon: 'Icon',\n            title: 'Tune',\n            // eslint-disable-next-line @typescript-eslint/no-empty-function\n            onActivate: () => {},\n          };\n        }\n\n        /**\n         * Tool can save it's data\n         */\n        public save(): { text: string; level: number } {\n          return {\n            text: 'Some text',\n            level: 1,\n          };\n        }\n      }\n\n      /** Editor instance with TestTool installed and one block of TestTool type */\n      cy.createEditor({\n        tools: {\n          testTool: TestTool,\n        },\n        data: {\n          blocks: [\n            {\n              type: 'testTool',\n              data: {\n                text: 'Some text',\n                level: 1,\n              },\n            },\n          ],\n        },\n      });\n\n      /** Open block tunes menu */\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-block')\n        .click();\n\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-toolbar__settings-btn')\n        .click();\n\n      /** Check there are more than 1 tune */\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-popover-item')\n        .should('have.length.above', 1);\n\n      /** Check the first tune is tool specific tune */\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-popover-item:first-child')\n        .contains('Tune')\n        .should('exist');\n    });\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/ui/DataEmpty.cy.ts",
    "content": "import { createEditorWithTextBlocks } from '../../support/utils/createEditorWithTextBlocks';\n\ndescribe('inputs [data-empty] mark', function () {\n  it('should be added to inputs of editor on initialization', function () {\n    createEditorWithTextBlocks([\n      'First', // not empty block\n      '', // empty block\n    ]);\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .first()\n      .should('have.attr', 'data-empty', 'false');\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .last()\n      .should('have.attr', 'data-empty', 'true');\n  });\n\n  it('should be added as \"false\" to the input on typing', function () {\n    createEditorWithTextBlocks([\n      'First', // not empty block\n      '', // empty block\n    ]);\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .last()\n      .type('Some text');\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .last()\n      .should('have.attr', 'data-empty', 'false');\n  });\n\n  it('should be added as \"true\" to the input on chars removal', function () {\n    createEditorWithTextBlocks([\n      '', // empty block\n      'Some text', // not empty block\n    ]);\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .last()\n      .type('{selectall}{backspace}');\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .last()\n      .should('have.attr', 'data-empty', 'true');\n  });\n\n  it('should be added to the new block inputs', function () {\n    createEditorWithTextBlocks([\n      'First', // not empty block\n      '', // empty block\n    ]);\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .last()\n      .type('{enter}');\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .last()\n      .should('have.attr', 'data-empty', 'true');\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/ui/InlineToolbar.cy.ts",
    "content": "import Header from '@editorjs/header';\nimport type { InlineTool, MenuConfig } from '../../../../types/tools';\nimport { createEditorWithTextBlocks } from '../../support/utils/createEditorWithTextBlocks';\n\ndescribe('Inline Toolbar', () => {\n  describe('Separators', () => {\n    it('should have a separator after the first item if it has children', () => {\n      cy.createEditor({\n        tools: {\n          header: {\n            class: Header,\n          },\n        },\n        data: {\n          blocks: [\n            {\n              type: 'paragraph',\n              data: {\n                text: 'First block text',\n              },\n            },\n          ],\n        },\n      });\n\n      /** Open Inline Toolbar */\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .selectText('block');\n\n      /** Check that first item (which is convert-to and has children) has a separator after it */\n      cy.get('[data-cy=editorjs]')\n        .get('[data-cy=inline-toolbar] .ce-popover__items')\n        .children()\n        .first()\n        .should('have.attr', 'data-item-name', 'convert-to');\n\n      cy.get('[data-cy=editorjs]')\n        .get('[data-cy=inline-toolbar] .ce-popover__items')\n        .children()\n        .eq(1)\n        .should('have.class', 'ce-popover-item-separator');\n    });\n\n    it('should have separators from both sides of item if it is in the middle and has children', () => {\n      cy.createEditor({\n        tools: {\n          header: {\n            class: Header,\n            inlineToolbar: ['bold', 'testTool', 'link'],\n\n          },\n          testTool: {\n            class: class {\n              public static isInline = true;\n              // eslint-disable-next-line jsdoc/require-jsdoc\n              public render(): MenuConfig {\n                return {\n                  icon: 'n',\n                  title: 'Test Tool',\n                  name: 'test-tool',\n                  children: {\n                    items: [\n                      {\n                        icon: 'm',\n                        title: 'Test Tool Item',\n                        // eslint-disable-next-line  @typescript-eslint/no-empty-function\n                        onActivate: () => {},\n                      },\n                    ],\n                  },\n                };\n              }\n            },\n          },\n        },\n        data: {\n          blocks: [\n            {\n              type: 'header',\n              data: {\n                text: 'First block text',\n              },\n            },\n          ],\n        },\n      });\n\n      /** Open Inline Toolbar */\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-header')\n        .selectText('block');\n\n      /** Check that item with children is surrounded by separators */\n      cy.get('[data-cy=editorjs]')\n        .get('[data-cy=inline-toolbar] .ce-popover__items')\n        .children()\n        .eq(3)\n        .should('have.class', 'ce-popover-item-separator');\n\n      cy.get('[data-cy=editorjs]')\n        .get('[data-cy=inline-toolbar] .ce-popover__items')\n        .children()\n        .eq(4)\n        .should('have.attr', 'data-item-name', 'test-tool');\n\n      cy.get('[data-cy=editorjs]')\n        .get('[data-cy=inline-toolbar] .ce-popover__items')\n        .children()\n        .eq(5)\n        .should('have.class', 'ce-popover-item-separator');\n    });\n\n    it('should have separator before the item with children if it is the last of all items', () => {\n      cy.createEditor({\n        tools: {\n          header: {\n            class: Header,\n            inlineToolbar: ['bold', 'testTool'],\n\n          },\n          testTool: {\n            class: class {\n              public static isInline = true;\n              // eslint-disable-next-line jsdoc/require-jsdoc\n              public render(): MenuConfig {\n                return {\n                  icon: 'n',\n                  title: 'Test Tool',\n                  name: 'test-tool',\n                  children: {\n                    items: [\n                      {\n                        icon: 'm',\n                        title: 'Test Tool Item',\n                        // eslint-disable-next-line  @typescript-eslint/no-empty-function\n                        onActivate: () => {},\n                      },\n                    ],\n                  },\n                };\n              }\n            },\n          },\n        },\n        data: {\n          blocks: [\n            {\n              type: 'header',\n              data: {\n                text: 'First block text',\n              },\n            },\n          ],\n        },\n      });\n\n      /** Open Inline Toolbar */\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-header')\n        .selectText('block');\n\n      /** Check that item with children is surrounded by separators */\n      cy.get('[data-cy=editorjs]')\n        .get('[data-cy=inline-toolbar] .ce-popover__items')\n        .children()\n        .eq(3)\n        .should('have.class', 'ce-popover-item-separator');\n\n      cy.get('[data-cy=editorjs]')\n        .get('[data-cy=inline-toolbar] .ce-popover__items')\n        .children()\n        .eq(4)\n        .should('have.attr', 'data-item-name', 'test-tool');\n    });\n  });\n\n  describe('Shortcuts', () => {\n    it('should work in read-only mode', () => {\n      const toolSurround = cy.stub().as('toolSurround');\n\n      /* eslint-disable jsdoc/require-jsdoc */\n      class Marker implements InlineTool {\n        public static isInline = true;\n        public static shortcut = 'CMD+SHIFT+M';\n        public static isReadOnlySupported = true;\n        public render(): MenuConfig {\n          return {\n            icon: 'm',\n            title: 'Marker',\n            onActivate: () => {\n              toolSurround();\n            },\n          };\n        }\n      }\n      /* eslint-enable jsdoc/require-jsdoc */\n\n      createEditorWithTextBlocks([\n        'some text',\n      ], {\n        tools: {\n          marker: Marker,\n        },\n        readOnly: true,\n      });\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .selectText('text');\n\n      cy.wait(300);\n\n      cy.document().then((doc) => {\n        doc.dispatchEvent(new KeyboardEvent('keydown', {\n          bubbles: true,\n          cancelable: true,\n          key: 'M',\n          code: 'KeyM',\n          keyCode: 77,\n          which: 77,\n          metaKey: true,\n          shiftKey: true,\n        }));\n      });\n\n      cy.get('@toolSurround').should('have.been.called');\n    });\n  });\n});\n\n"
  },
  {
    "path": "test/cypress/tests/ui/Placeholders.cy.ts",
    "content": "/**\n * Text will be passed as a placeholder to the editor\n */\nconst PLACEHOLDER_TEXT = 'Write something or press / to select a tool';\n\ndescribe('Placeholders', function () {\n  /**\n   * There is no ability to get pseudo elements content in Firefox\n   * It will return CSS-bases value (attr(data-placeholder) instead of DOM-based\n   */\n  if (Cypress.browser.family === 'firefox') {\n    return;\n  }\n\n  it('should be shown near first block if passed via editor config', function () {\n    cy.createEditor({\n      placeholder: PLACEHOLDER_TEXT,\n    });\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .getPseudoElementContent('::before')\n      .should('eq', PLACEHOLDER_TEXT);\n  });\n\n  it('should be shown when editor is autofocusable', function () {\n    cy.createEditor({\n      placeholder: PLACEHOLDER_TEXT,\n      autofocus: true,\n    });\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .getPseudoElementContent('::before')\n      .should('eq', PLACEHOLDER_TEXT);\n  });\n\n  it('should be shown event if input is focused', function () {\n    cy.createEditor({\n      placeholder: PLACEHOLDER_TEXT,\n    });\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .click()\n      .as('firstBlock')\n      .getPseudoElementContent('::before')\n      .should('eq', PLACEHOLDER_TEXT);\n  });\n\n  it('should be shown event when user removes all text by cmd+a and delete', function () {\n    cy.createEditor({\n      placeholder: PLACEHOLDER_TEXT,\n    });\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .type('aaa')\n      .type('{selectall}{backspace}')\n      .getPseudoElementContent('::before')\n      .should('eq', PLACEHOLDER_TEXT);\n  });\n\n  it('should be hidden when user starts typing', function () {\n    cy.createEditor({\n      placeholder: PLACEHOLDER_TEXT,\n    });\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .as('firstBlock')\n      .getPseudoElementContent('::before')\n      .should('eq', PLACEHOLDER_TEXT);\n\n    cy.get('@firstBlock')\n      .type('a')\n      .getPseudoElementContent('::before')\n      .should('eq', 'none');\n  });\n\n  it('should be hidden when user adds trailing whitespace characters', function () {\n    cy.createEditor({\n      placeholder: PLACEHOLDER_TEXT,\n    });\n\n    cy.get('[data-cy=editorjs]')\n      .find('.ce-paragraph')\n      .as('firstBlock')\n      .getPseudoElementContent('::before')\n      .should('eq', PLACEHOLDER_TEXT);\n\n    cy.get('@firstBlock')\n      .type('   ')\n      .getPseudoElementContent('::before')\n      .should('eq', 'none');\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/ui/toolbox.cy.ts",
    "content": "import type EditorJS from '../../../../types/index';\nimport type { ConversionConfig, ToolboxConfig } from '../../../../types/index';\nimport ToolMock from '../../fixtures/tools/ToolMock';\n\ndescribe('Toolbox', function () {\n  describe('Shortcuts', function () {\n    it('should convert current Block to the Shortcuts\\'s Block if both tools provides a \"conversionConfig\". Caret should be restored after conversion.', function () {\n      /**\n       * Mock of Tool with conversionConfig\n       */\n      class ConvertableTool extends ToolMock {\n        /**\n         * Specify how to import string data to this Tool\n         */\n        public static get conversionConfig(): ConversionConfig {\n          return {\n            import: 'text',\n          };\n        }\n\n        /**\n         * Specify how to display Tool in a Toolbox\n         */\n        public static get toolbox(): ToolboxConfig {\n          return {\n            icon: '',\n            title: 'Convertable tool',\n          };\n        }\n      }\n\n      cy.createEditor({\n        tools: {\n          convertableTool: {\n            class: ConvertableTool,\n            shortcut: 'CMD+SHIFT+H',\n          },\n        },\n      }).as('editorInstance');\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .click()\n        .type('Some text')\n        .type('{cmd}{shift}H'); // call a shortcut\n\n      /**\n       * Check that block was converted\n       */\n      cy.get<EditorJS>('@editorInstance')\n        .then(async (editor) => {\n          const { blocks } = await editor.save();\n\n          expect(blocks.length).to.eq(1);\n          expect(blocks[0].type).to.eq('convertableTool');\n          expect(blocks[0].data.text).to.eq('Some text');\n\n          /**\n           * Check that caret belongs to the new block after conversion\n           */\n          cy.window()\n            .then((window) => {\n              const selection = window.getSelection();\n              const range = selection.getRangeAt(0);\n\n              cy.get('[data-cy=editorjs]')\n                .find(`.ce-block[data-id=${blocks[0].id}]`)\n                .should(($block) => {\n                  expect($block[0].contains(range.startContainer)).to.be.true;\n                });\n            });\n        });\n    });\n\n    it('should insert a Shortcuts\\'s Block below the current if some (original or target) tool does not provide a \"conversionConfig\" ', function () {\n      /**\n       * Mock of Tool with conversionConfig\n       */\n      class ToolWithoutConversionConfig extends ToolMock {\n        /**\n         * Specify how to display Tool in a Toolbox\n         */\n        public static get toolbox(): ToolboxConfig {\n          return {\n            icon: '',\n            title: 'Convertable tool',\n          };\n        }\n      }\n\n      cy.createEditor({\n        tools: {\n          nonConvertableTool: {\n            class: ToolWithoutConversionConfig,\n            shortcut: 'CMD+SHIFT+H',\n          },\n        },\n      }).as('editorInstance');\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .click()\n        .type('Some text')\n        .type('{cmd}{shift}H'); // call a shortcut\n\n      /**\n       * Check that the new block was appended\n       */\n      cy.get<EditorJS>('@editorInstance')\n        .then(async (editor) => {\n          const { blocks } = await editor.save();\n\n          expect(blocks.length).to.eq(2);\n          expect(blocks[1].type).to.eq('nonConvertableTool');\n        });\n    });\n\n    it('should display shortcut only for the first toolbox item if tool exports toolbox with several items', function () {\n      /**\n       * Mock of Tool with conversionConfig\n       */\n      class ToolWithSeveralToolboxItems extends ToolMock {\n        /**\n         * Specify toolbox with several items related to one tool\n         */\n        public static get toolbox(): ToolboxConfig {\n          return [\n            {\n              icon: '',\n              title: 'first tool',\n            },\n            {\n              icon: '',\n              title: 'second tool',\n            },\n          ];\n        }\n      }\n\n      cy.createEditor({\n        tools: {\n          severalToolboxItemsTool: {\n            class: ToolWithSeveralToolboxItems,\n            shortcut: 'CMD+SHIFT+L',\n          },\n        },\n      });\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .click()\n        .type('Some text')\n        .type('/'); // call a shortcut for toolbox\n\n      /**\n       * Secondary title (shortcut) should exist for first toolbox item of the tool\n       */\n      /* eslint-disable-next-line cypress/require-data-selectors */\n      cy.get('.ce-popover')\n        .find('.ce-popover-item[data-item-name=\"severalToolboxItemsTool\"]')\n        .first()\n        .find('.ce-popover-item__secondary-title')\n        .should('exist');\n\n      /**\n       * Secondary title (shortcut) should not exist for second toolbox item of the same tool\n       */\n      /* eslint-disable-next-line cypress/require-data-selectors */\n      cy.get('.ce-popover')\n        .find('.ce-popover-item[data-item-name=\"severalToolboxItemsTool\"]')\n        .eq(1)\n        .find('.ce-popover-item__secondary-title')\n        .should('not.exist');\n    });\n\n    it('should display shortcut for the item if tool exports toolbox as an one item object', function () {\n      /**\n       * Mock of Tool with conversionConfig\n       */\n      class ToolWithOneToolboxItems extends ToolMock {\n        /**\n         * Specify toolbox with several items related to one tool\n         */\n        public static get toolbox(): ToolboxConfig {\n          return {\n            icon: '',\n            title: 'tool',\n          };\n        }\n      }\n\n      cy.createEditor({\n        tools: {\n          oneToolboxItemTool: {\n            class: ToolWithOneToolboxItems,\n            shortcut: 'CMD+SHIFT+L',\n          },\n        },\n      });\n\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .click()\n        .type('Some text')\n        .type('/'); // call a shortcut for toolbox\n\n      /**\n       * Secondary title (shortcut) should exist for toolbox item of the tool\n       */\n      /* eslint-disable-next-line cypress/require-data-selectors */\n      cy.get('.ce-popover')\n        .find('.ce-popover-item[data-item-name=\"oneToolboxItemTool\"]')\n        .first()\n        .find('.ce-popover-item__secondary-title')\n        .should('exist');\n    });\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/utils/flipper.cy.ts",
    "content": "import type { PopoverItemParams } from '../../../../types/index.js';\n\n/**\n * Mock of some Block Tool\n */\nclass SomePlugin {\n  /**\n   * Event handler to be spied in test\n   */\n  // eslint-disable-next-line @typescript-eslint/no-empty-function\n  public static pluginInternalKeydownHandler(): void {}\n\n  /**\n   * Mocked render method\n   */\n  public render(): HTMLElement {\n    const wrapper = document.createElement('div');\n\n    wrapper.classList.add('cdx-some-plugin');\n    wrapper.contentEditable = 'true';\n    wrapper.addEventListener('keydown', SomePlugin.pluginInternalKeydownHandler);\n\n    return wrapper;\n  }\n\n  /**\n   * Used to display our tool in the Toolbox\n   */\n  public static get toolbox(): PopoverItemParams {\n    return {\n      icon: '₷',\n      title: 'Some tool',\n      // eslint-disable-next-line @typescript-eslint/no-empty-function\n      onActivate: (): void => {},\n    };\n  }\n\n  /**\n   * Extracts data from the plugin's UI\n   */\n  public save(): {data: string} {\n    return {\n      data: '123',\n    };\n  }\n}\n\ndescribe('Flipper', () => {\n  const ARROW_DOWN_KEY_CODE = 40;\n  const ENTER_KEY_CODE = 13;\n\n  it('should prevent plugins event handlers from being called while keyboard navigation', () => {\n    const sampleText = 'sample text';\n\n    cy.createEditor({\n      tools: {\n        sometool: SomePlugin,\n      },\n      data: {\n        blocks: [\n          {\n            type: 'sometool',\n            data: {\n            },\n          },\n        ],\n      },\n    });\n\n    cy.spy(SomePlugin, 'pluginInternalKeydownHandler');\n\n    cy.get('[data-cy=editorjs]')\n      .get('.cdx-some-plugin')\n      .as('pluginInput')\n      .focus()\n      .type(sampleText)\n      .wait(100);\n\n    // Try to delete the block via keyboard\n    cy.get('[data-cy=editorjs]')\n      .get('.cdx-some-plugin')\n      // Open tunes menu\n      .trigger('keydown', { code: 'Slash',\n        ctrlKey: true })\n      // Navigate to delete button (the second button)\n      .trigger('keydown', { keyCode: ARROW_DOWN_KEY_CODE })\n      .trigger('keydown', { keyCode: ARROW_DOWN_KEY_CODE });\n\n    /**\n     * Check whether we focus the Delete Tune or not\n     */\n    cy.get('[data-item-name=\"delete\"]')\n      .should('have.class', 'ce-popover-item--focused');\n\n    cy.get('[data-cy=editorjs]')\n      .get('.cdx-some-plugin')\n      // Click delete\n      .trigger('keydown', { keyCode: ENTER_KEY_CODE })\n      // // Confirm delete\n      .trigger('keydown', { keyCode: ENTER_KEY_CODE });\n\n    expect(SomePlugin.pluginInternalKeydownHandler).to.have.not.been.called;\n  });\n\n  it('should not flip when shift key is pressed', () => {\n    cy.createEditor({\n      data: {\n        blocks: [\n          {\n            type: 'paragraph',\n            data: {\n              text: 'Workspace in classic editors is made of a single contenteditable element, used to create different HTML markups. Editor.js workspace consists of separate Blocks: paragraphs, headings, images, lists, quotes, etc. Each of them is an independent contenteditable element (or more complex structure) provided by Plugin and united by Editor\\'s Core.',\n            },\n          },\n        ],\n      },\n      autofocus: true,\n    });\n\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-paragraph')\n      .as('paragraph')\n      .selectTextByOffset([0, 10])\n      .wait(200);\n\n    cy.get('@paragraph')\n      .trigger('keydown', { keyCode: ARROW_DOWN_KEY_CODE,\n        shiftKey: true });\n\n    // eslint-disable-next-line cypress/require-data-selectors\n    cy.get('[data-cy=\"inline-toolbar\"]')\n      .get('.ce-popover--opened')\n      .as('popover')\n      .should('exist');\n\n    cy.get('@popover')\n      .get('.ce-popover-item--focused')\n      .should('not.exist');\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/utils/popover.cy.ts",
    "content": "import { PopoverDesktop as Popover, PopoverItemType } from '../../../../src/components/utils/popover';\nimport type { PopoverItemParams } from '@/types/utils/popover';\nimport type { MenuConfig } from '../../../../types/tools';\nimport Header from '@editorjs/header';\n\n/* eslint-disable @typescript-eslint/no-empty-function */\n\ndescribe('Popover', () => {\n  it('should support confirmation chains', () => {\n    const actionIcon = 'Icon 1';\n    const actionTitle = 'Action';\n    const confirmActionIcon = 'Icon 2';\n    const confirmActionTitle = 'Confirm action';\n\n    /**\n     * Confirmation is moved to separate variable to be able to test it's callback execution.\n     * (Inside popover null value is set to confirmation property, so, object becomes unavailable otherwise)\n     */\n    const confirmation: PopoverItemParams = {\n      icon: confirmActionIcon,\n      title: confirmActionTitle,\n      onActivate: cy.stub(),\n    };\n\n    const items: PopoverItemParams[] = [\n      {\n        icon: actionIcon,\n        title: actionTitle,\n        name: 'testItem',\n        confirmation,\n      },\n    ];\n\n    const popover = new Popover({\n      items,\n    });\n\n    cy.document().then(doc => {\n      doc.body.append(popover.getElement());\n\n      cy.get('[data-item-name=testItem]')\n        .get('.ce-popover-item__icon')\n        .should('have.text', actionIcon);\n\n      cy.get('[data-item-name=testItem]')\n        .get('.ce-popover-item__title')\n        .should('have.text', actionTitle);\n\n      // First click on item\n      cy.get('[data-item-name=testItem]').click();\n\n      // Check icon has changed\n      cy.get('[data-item-name=testItem]')\n        .get('.ce-popover-item__icon')\n        .should('have.text', confirmActionIcon);\n\n      // Check label has changed\n      cy.get('[data-item-name=testItem]')\n        .get('.ce-popover-item__title')\n        .should('have.text', confirmActionTitle);\n\n      // Second click\n      cy.get('[data-item-name=testItem]')\n        .click()\n        .then(() => {\n          // Check onActivate callback has been called\n          expect(confirmation.onActivate).to.have.been.calledOnce;\n        });\n    });\n  });\n\n  it('should render the items with true isActive property value as active', () => {\n    const items = [\n      {\n        icon: 'Icon',\n        title: 'Title',\n        isActive: true,\n        name: 'testItem',\n        onActivate: (): void => {},\n      },\n    ];\n\n    const popover = new Popover({\n      items,\n    });\n\n    cy.document().then(doc => {\n      doc.body.append(popover.getElement());\n\n      /* Check item has active class */\n      cy.get('[data-item-name=testItem]')\n        .should('have.class', 'ce-popover-item--active');\n    });\n  });\n\n  it('should not execute item\\'s onActivate callback if the item is disabled', () => {\n    const items: PopoverItemParams[] = [\n      {\n        icon: 'Icon',\n        title: 'Title',\n        isDisabled: true,\n        name: 'testItem',\n        onActivate: cy.stub(),\n      },\n    ];\n\n    const popover = new Popover({\n      items,\n    });\n\n    cy.document().then(doc => {\n      doc.body.append(popover.getElement());\n\n      /* Check item has disabled class */\n      cy.get('[data-item-name=testItem]')\n        .should('have.class', 'ce-popover-item--disabled')\n        .click()\n        .then(() => {\n          if (items[0].type !== PopoverItemType.Default) {\n            return;\n          }\n\n          // Check onActivate callback has never been called\n          expect(items[0].onActivate).to.have.not.been.called;\n        });\n    });\n  });\n\n  it('should close once item with closeOnActivate property set to true is activated', () => {\n    const items = [\n      {\n        icon: 'Icon',\n        title: 'Title',\n        closeOnActivate: true,\n        name: 'testItem',\n        onActivate: (): void => {},\n      },\n    ];\n    const popover = new Popover({\n      items,\n    });\n\n    cy.spy(popover, 'hide');\n\n    cy.document().then(doc => {\n      doc.body.append(popover.getElement());\n\n      cy.get('[data-item-name=testItem]')\n        .click()\n        .then(() => {\n          expect(popover.hide).to.have.been.called;\n        });\n    });\n  });\n\n  it('should highlight as active the item with toggle property set to true once activated', () => {\n    const items = [\n      {\n        icon: 'Icon',\n        title: 'Title',\n        toggle: true,\n        name: 'testItem',\n        onActivate: (): void => {},\n      },\n    ];\n    const popover = new Popover({\n      items,\n    });\n\n    cy.document().then(doc => {\n      doc.body.append(popover.getElement());\n\n      /* Check item has active class */\n      cy.get('[data-item-name=testItem]')\n        .click()\n        .should('have.class', 'ce-popover-item--active');\n    });\n  });\n\n  it('should perform radiobutton-like behavior among the items that have toggle property value set to the same string value', () => {\n    const items = [\n      {\n        icon: 'Icon 1',\n        title: 'Title 1',\n        toggle: 'group-name',\n        name: 'testItem1',\n        isActive: true,\n        onActivate: (): void => {},\n      },\n      {\n        icon: 'Icon 2',\n        title: 'Title 2',\n        toggle: 'group-name',\n        name: 'testItem2',\n        onActivate: (): void => {},\n      },\n    ];\n\n    const popover = new Popover({\n      items,\n    });\n\n    cy.document().then(doc => {\n      doc.body.append(popover.getElement());\n\n      /** Check first item is active */\n      cy.get('[data-item-name=testItem1]')\n        .should('have.class', 'ce-popover-item--active');\n\n      /** Check second item is not active */\n      cy.get('[data-item-name=testItem2]')\n        .should('not.have.class', 'ce-popover-item--active');\n\n      /* Click second item and check it became active */\n      cy.get('[data-item-name=testItem2]')\n        .click()\n        .should('have.class', 'ce-popover-item--active');\n\n      /** Check first item became not active */\n      cy.get('[data-item-name=testItem1]')\n        .should('not.have.class', 'ce-popover-item--active');\n    });\n  });\n\n  it('should toggle item if it is the only item in toggle group', () => {\n    const items = [\n      {\n        icon: 'Icon',\n        title: 'Title',\n        toggle: 'key',\n        name: 'testItem',\n        onActivate: (): void => {},\n      },\n    ];\n    const popover = new Popover({\n      items,\n    });\n\n    cy.document().then(doc => {\n      doc.body.append(popover.getElement());\n\n      /* Check item has active class */\n      cy.get('[data-item-name=testItem]')\n        .click()\n        .should('have.class', 'ce-popover-item--active');\n    });\n  });\n\n  it('should display item with custom html', () => {\n    /**\n     * Block Tune with html as return type of render() method\n     */\n    class TestTune {\n      public static isTune = true;\n\n      /** Tune control displayed in block tunes popover */\n      public render(): HTMLElement {\n        const button = document.createElement('button');\n\n        button.classList.add('ce-settings__button');\n        button.innerText = 'Tune';\n\n        return button;\n      }\n    }\n\n    /** Create editor instance */\n    cy.createEditor({\n      tools: {\n        testTool: TestTune,\n      },\n      tunes: [ 'testTool' ],\n      data: {\n        blocks: [\n          {\n            type: 'paragraph',\n            data: {\n              text: 'Hello',\n            },\n          },\n        ],\n      },\n    });\n\n    /** Open block tunes menu */\n    cy.get('[data-cy=editorjs]')\n      .get('.cdx-block')\n      .click();\n\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-toolbar__settings-btn')\n      .click();\n\n    /** Check item with custom html content is displayed */\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-popover .ce-popover-item-html')\n      .contains('Tune')\n      .should('be.visible');\n  });\n\n  it('should support flipping between custom content items', () => {\n    /**\n     * Block Tune with html as return type of render() method\n     */\n    class TestTune1 {\n      public static isTune = true;\n\n      /** Tune control displayed in block tunes popover */\n      public render(): HTMLElement {\n        const button = document.createElement('button');\n\n        button.classList.add('ce-settings__button');\n        button.innerText = 'Tune1';\n\n        return button;\n      }\n    }\n\n    /**\n     * Block Tune with html as return type of render() method\n     */\n    class TestTune2 {\n      public static isTune = true;\n\n      /** Tune control displayed in block tunes popover */\n      public render(): HTMLElement {\n        const button = document.createElement('button');\n\n        button.classList.add('ce-settings__button');\n        button.innerText = 'Tune2';\n\n        return button;\n      }\n    }\n\n    /** Create editor instance */\n    cy.createEditor({\n      tools: {\n        testTool1: TestTune1,\n        testTool2: TestTune2,\n      },\n      tunes: ['testTool1', 'testTool2'],\n      data: {\n        blocks: [\n          {\n            type: 'paragraph',\n            data: {\n              text: 'Hello',\n            },\n          },\n        ],\n      },\n    });\n\n    /** Open block tunes menu */\n    cy.get('[data-cy=editorjs]')\n      .get('.cdx-block')\n      .click();\n\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-toolbar__settings-btn')\n      .click();\n\n    /** Press Tab */\n    // eslint-disable-next-line cypress/require-data-selectors -- cy.tab() not working here\n    cy.get('body').tab();\n\n    /** Check the first custom html item is focused */\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-popover .ce-popover-item-html .ce-settings__button')\n      .contains('Tune1')\n      .should('have.class', 'ce-popover-item--focused');\n\n    /** Press Tab */\n    // eslint-disable-next-line cypress/require-data-selectors -- cy.tab() not working here\n    cy.get('body').tab();\n\n    /** Check the second custom html item is focused */\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-popover .ce-popover-item-html .ce-settings__button')\n      .contains('Tune2')\n      .should('have.class', 'ce-popover-item--focused');\n\n    /** Press Tab */\n    // eslint-disable-next-line cypress/require-data-selectors -- cy.tab() not working here\n    cy.get('body').tab();\n\n    /** Check that default popover item got focused */\n    cy.get('[data-cy=editorjs]')\n      .get('[data-item-name=move-up]')\n      .should('have.class', 'ce-popover-item--focused');\n  });\n\n  it('should display nested popover (desktop)', () => {\n    /** Tool class to test how it is displayed inside block tunes popover */\n    class TestTune {\n      public static isTune = true;\n\n      /** Tool data displayed in block tunes popover */\n      public render(): MenuConfig {\n        return  {\n          icon: 'Icon',\n          title: 'Title',\n          toggle: 'key',\n          name: 'test-item',\n          children: {\n            items: [\n              {\n                icon: 'Icon',\n                title: 'Title',\n                name: 'nested-test-item',\n                onActivate: (): void => {},\n              },\n            ],\n          },\n        };\n      }\n    }\n\n    /** Create editor instance */\n    cy.createEditor({\n      tools: {\n        testTool: TestTune,\n      },\n      tunes: [ 'testTool' ],\n      data: {\n        blocks: [\n          {\n            type: 'paragraph',\n            data: {\n              text: 'Hello',\n            },\n          },\n        ],\n      },\n    });\n\n    /** Open block tunes menu */\n    cy.get('[data-cy=editorjs]')\n      .get('.cdx-block')\n      .click();\n\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-toolbar__settings-btn')\n      .click();\n\n    /** Check item with children has arrow icon */\n    cy.get('[data-cy=editorjs]')\n      .get('[data-item-name=\"test-item\"]')\n      .get('.ce-popover-item__icon--chevron-right')\n      .should('be.visible');\n\n    /** Click the item */\n    cy.get('[data-cy=editorjs]')\n      .get('[data-item-name=\"test-item\"]')\n      .click();\n\n    /** Check nested popover opened */\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-popover--nested .ce-popover__container')\n      .should('be.visible');\n\n    /** Check child item displayed */\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-popover--nested .ce-popover__container')\n      .get('[data-item-name=\"nested-test-item\"]')\n      .should('be.visible');\n  });\n\n  it('should display children items, back button and item header and correctly switch between parent and child states (mobile)', () => {\n    /** Tool class to test how it is displayed inside block tunes popover */\n    class TestTune {\n      public static isTune = true;\n\n      /** Tool data displayed in block tunes popover */\n      public render(): MenuConfig {\n        return  {\n          icon: 'Icon',\n          title: 'Tune',\n          toggle: 'key',\n          name: 'test-item',\n          children: {\n            items: [\n              {\n                icon: 'Icon',\n                title: 'Title',\n                name: 'nested-test-item',\n                onActivate: (): void => {},\n              },\n            ],\n          },\n        };\n      }\n    }\n\n    cy.viewport('iphone-6+');\n\n\n    /** Create editor instance */\n    cy.createEditor({\n      tools: {\n        testTool: TestTune,\n      },\n      tunes: [ 'testTool' ],\n      data: {\n        blocks: [\n          {\n            type: 'paragraph',\n            data: {\n              text: 'Hello',\n            },\n          },\n        ],\n      },\n    });\n\n    /** Open block tunes menu */\n    cy.get('[data-cy=editorjs]')\n      .get('.cdx-block')\n      .click();\n\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-toolbar__settings-btn')\n      .click();\n\n    /** Check item with children has arrow icon */\n    cy.get('[data-cy=editorjs]')\n      .get('[data-item-name=\"test-item\"]')\n      .get('.ce-popover-item__icon--chevron-right')\n      .should('be.visible');\n\n    /** Click the item */\n    cy.get('[data-cy=editorjs]')\n      .get('[data-item-name=\"test-item\"]')\n      .click();\n\n    /** Check child item displayed */\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-popover__container')\n      .get('[data-item-name=\"nested-test-item\"]')\n      .should('be.visible');\n\n    /** Check header displayed */\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-popover-header')\n      .should('have.text', 'Tune');\n\n    /** Check back button displayed */\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-popover__container')\n      .get('.ce-popover-header__back-button')\n      .should('be.visible');\n\n    /** Click back button */\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-popover__container')\n      .get('.ce-popover-header__back-button')\n      .click();\n\n    /** Check child item is not displayed */\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-popover__container')\n      .get('[data-item-name=\"nested-test-item\"]')\n      .should('not.exist');\n\n    /** Check back button is not displayed */\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-popover__container')\n      .get('.ce-popover-header__back-button')\n      .should('not.exist');\n\n    /** Check header is not displayed */\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-popover-header')\n      .should('not.exist');\n  });\n\n\n  it('should display default (non-separator) items without specifying type: default', () => {\n    /** Tool class to test how it is displayed inside block tunes popover */\n    class TestTune {\n      public static isTune = true;\n\n      /** Tool data displayed in block tunes popover */\n      public render(): MenuConfig {\n        return  {\n          onActivate: (): void => {},\n          icon: 'Icon',\n          title: 'Tune',\n          toggle: 'key',\n          name: 'test-item',\n        };\n      }\n    }\n\n    /** Create editor instance */\n    cy.createEditor({\n      tools: {\n        testTool: TestTune,\n      },\n      tunes: [ 'testTool' ],\n      data: {\n        blocks: [\n          {\n            type: 'paragraph',\n            data: {\n              text: 'Hello',\n            },\n          },\n        ],\n      },\n    });\n\n    /** Open block tunes menu */\n    cy.get('[data-cy=editorjs]')\n      .get('.cdx-block')\n      .click();\n\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-toolbar__settings-btn')\n      .click();\n\n    /** Check item displayed */\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-popover__container')\n      .get('[data-item-name=\"test-item\"]')\n      .should('be.visible');\n  });\n\n  it('should display separator', () => {\n    /** Tool class to test how it is displayed inside block tunes popover */\n    class TestTune {\n      public static isTune = true;\n\n      /** Tool data displayed in block tunes popover */\n      public render(): MenuConfig {\n        return  [\n          {\n            onActivate: (): void => {},\n            icon: 'Icon',\n            title: 'Tune',\n            toggle: 'key',\n            name: 'test-item',\n          },\n          {\n            type: PopoverItemType.Separator,\n          },\n        ];\n      }\n    }\n\n\n    /** Create editor instance */\n    cy.createEditor({\n      tools: {\n        testTool: TestTune,\n      },\n      tunes: [ 'testTool' ],\n      data: {\n        blocks: [\n          {\n            type: 'paragraph',\n            data: {\n              text: 'Hello',\n            },\n          },\n        ],\n      },\n    });\n\n    /** Open block tunes menu */\n    cy.get('[data-cy=editorjs]')\n      .get('.cdx-block')\n      .click();\n\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-toolbar__settings-btn')\n      .click();\n\n    /** Check item displayed */\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-popover__container')\n      .get('[data-item-name=\"test-item\"]')\n      .should('be.visible');\n\n    /** Check separator displayed */\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-popover__container')\n      .get('.ce-popover-item-separator')\n      .should('be.visible');\n  });\n\n  it('should perform keyboard navigation between items ignoring separators', () => {\n    /** Tool class to test how it is displayed inside block tunes popover */\n    class TestTune {\n      public static isTune = true;\n\n      /** Tool data displayed in block tunes popover */\n      public render(): MenuConfig {\n        return  [\n          {\n            onActivate: (): void => {},\n            icon: 'Icon',\n            title: 'Tune 1',\n            name: 'test-item-1',\n          },\n          {\n            type: PopoverItemType.Separator,\n          },\n          {\n            onActivate: (): void => {},\n            icon: 'Icon',\n            title: 'Tune 2',\n            name: 'test-item-2',\n          },\n        ];\n      }\n    }\n\n    /** Create editor instance */\n    cy.createEditor({\n      tools: {\n        testTool: TestTune,\n      },\n      tunes: [ 'testTool' ],\n      data: {\n        blocks: [\n          {\n            type: 'paragraph',\n            data: {\n              text: 'Hello',\n            },\n          },\n        ],\n      },\n    });\n\n    /** Open block tunes menu */\n    cy.get('[data-cy=editorjs]')\n      .get('.cdx-block')\n      .click();\n\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-toolbar__settings-btn')\n      .click();\n\n    /** Press Tab */\n    // eslint-disable-next-line cypress/require-data-selectors -- cy.tab() not working here\n    cy.get('body').tab();\n\n    /** Check first item is focused */\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-popover__container')\n      .get('[data-item-name=\"test-item-1\"].ce-popover-item--focused')\n      .should('exist');\n\n    /** Check second item is not focused */\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-popover__container')\n      .get('[data-item-name=\"test-item-2\"].ce-popover-item--focused')\n      .should('not.exist');\n\n    /** Press Tab */\n    // eslint-disable-next-line cypress/require-data-selectors -- cy.tab() not working here\n    cy.get('body').tab();\n\n    /** Check first item is not focused */\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-popover__container')\n      .get('[data-item-name=\"test-item-1\"].ce-popover-item--focused')\n      .should('not.exist');\n\n    /** Check second item is focused */\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-popover__container')\n      .get('[data-item-name=\"test-item-2\"].ce-popover-item--focused')\n      .should('exist');\n  });\n\n  it('should perform keyboard navigation between items ignoring separators when search query is applied', () => {\n    /** Tool class to test how it is displayed inside block tunes popover */\n    class TestTune {\n      public static isTune = true;\n\n      /** Tool data displayed in block tunes popover */\n      public render(): MenuConfig {\n        return  [\n          {\n            onActivate: (): void => {},\n            icon: 'Icon',\n            title: 'Tune 1',\n            name: 'test-item-1',\n          },\n          {\n            type: PopoverItemType.Separator,\n          },\n          {\n            onActivate: (): void => {},\n            icon: 'Icon',\n            title: 'Tune 2',\n            name: 'test-item-2',\n          },\n        ];\n      }\n    }\n\n    /** Create editor instance */\n    cy.createEditor({\n      tools: {\n        testTool: TestTune,\n      },\n      tunes: [ 'testTool' ],\n      data: {\n        blocks: [\n          {\n            type: 'paragraph',\n            data: {\n              text: 'Hello',\n            },\n          },\n        ],\n      },\n    });\n\n    /** Open block tunes menu */\n    cy.get('[data-cy=editorjs]')\n      .get('.cdx-block')\n      .click();\n\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-toolbar__settings-btn')\n      .click();\n\n    /** Check separator displayed */\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-popover__container')\n      .get('.ce-popover-item-separator')\n      .should('be.visible');\n\n    /** Enter search query */\n    cy.get('[data-cy=editorjs]')\n      .get('[data-cy=block-tunes] .cdx-search-field__input')\n      .type('Tune');\n\n    /** Check separator not displayed */\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-popover__container')\n      .get('.ce-popover-item-separator')\n      .should('not.be.visible');\n\n    /** Press Tab */\n    // eslint-disable-next-line cypress/require-data-selectors -- cy.tab() not working here\n    cy.get('body').tab();\n\n    /** Check first item is focused */\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-popover__container')\n      .get('[data-item-name=\"test-item-1\"].ce-popover-item--focused')\n      .should('exist');\n\n    /** Check second item is not focused */\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-popover__container')\n      .get('[data-item-name=\"test-item-2\"].ce-popover-item--focused')\n      .should('not.exist');\n\n    /** Press Tab */\n    // eslint-disable-next-line cypress/require-data-selectors -- cy.tab() not working here\n    cy.get('body').tab();\n\n    /** Check first item is not focused */\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-popover__container')\n      .get('[data-item-name=\"test-item-1\"].ce-popover-item--focused')\n      .should('not.exist');\n\n    /** Check second item is focused */\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-popover__container')\n      .get('[data-item-name=\"test-item-2\"].ce-popover-item--focused')\n      .should('exist');\n  });\n\n  it('shoould support i18n in nested popover', () => {\n    /**\n     *\n     */\n    class TestTune {\n      public static isTune = true;\n\n      /** Tool data displayed in block tunes popover */\n      public render(): MenuConfig {\n        return  {\n          icon: 'Icon',\n          title: 'Title',\n          toggle: 'key',\n          name: 'test-item',\n          children: {\n            searchable: true,\n            items: [\n              {\n                icon: 'Icon',\n                title: 'Title',\n                name: 'nested-test-item',\n                onActivate: (): void => {},\n              },\n            ],\n          },\n        };\n      }\n    }\n\n    /** Create editor instance */\n    cy.createEditor({\n      tools: {\n        testTool: TestTune,\n      },\n      tunes: [ 'testTool' ],\n      data: {\n        blocks: [\n          {\n            type: 'paragraph',\n            data: {\n              text: 'Hello',\n            },\n          },\n        ],\n      },\n      i18n: {\n        messages: {\n          ui: {\n            popover: {\n              'Filter': 'Искать',\n              // eslint-disable-next-line @typescript-eslint/naming-convention -- i18n\n              'Nothing found': 'Ничего не найдено',\n            },\n          },\n        },\n      },\n    });\n\n    /** Open block tunes menu */\n    cy.get('[data-cy=editorjs]')\n      .get('.cdx-block')\n      .click();\n\n    cy.get('[data-cy=editorjs]')\n      .get('.ce-toolbar__settings-btn')\n      .click();\n\n    /** Click the item */\n    cy.get('[data-cy=editorjs]')\n      .get('[data-item-name=\"test-item\"]')\n      .click();\n\n    /** Check nested popover search input has placeholder text with i18n */\n    cy.get('[data-cy=editorjs]')\n      .get('[data-cy=block-tunes] .ce-popover--nested .cdx-search-field__input')\n      .invoke('attr', 'placeholder')\n      .should('eq', 'Искать');\n\n    /** Enter search query */\n    cy.get('[data-cy=editorjs]')\n      .get('[data-cy=block-tunes] .ce-popover--nested .cdx-search-field__input')\n      .type('Some text');\n\n    /** Check nested popover has nothing found message with i18n */\n    cy.get('[data-cy=editorjs]')\n      .get('[data-cy=block-tunes] .ce-popover--nested .ce-popover__nothing-found-message')\n      .should('have.text', 'Ничего не найдено');\n  });\n\n  describe('Inline Popover', () => {\n    it('should open nested popover on click instead of hover', () => {\n      cy.createEditor({\n        tools: {\n          header: {\n            class: Header,\n          },\n        },\n        data: {\n          blocks: [\n            {\n              type: 'paragraph',\n              data: {\n                text: 'First block text',\n              },\n            },\n          ],\n        },\n      });\n\n      /** Open Inline Toolbar */\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .selectText('block');\n\n      /** Hover Convert To item which has nested popover */\n      cy.get('[data-cy=editorjs]')\n        .get('[data-item-name=convert-to]')\n        .trigger('mouseover');\n\n      /** Check nested popover didn't open */\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-popover--nested .ce-popover__container')\n        .should('not.exist');\n\n      /** Click Convert To item which has nested popover */\n      cy.get('[data-cy=editorjs]')\n        .get('[data-item-name=convert-to]')\n        .click();\n\n      /** Check nested popover opened */\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-popover--nested .ce-popover__container')\n        .should('exist');\n    });\n\n    it('should support keyboard nevigation between items', () => {\n      cy.createEditor({\n        tools: {\n          header: {\n            class: Header,\n          },\n        },\n        data: {\n          blocks: [\n            {\n              type: 'paragraph',\n              data: {\n                text: 'First block text',\n              },\n            },\n          ],\n        },\n      });\n\n      /** Open Inline Toolbar */\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .selectText('block');\n\n      /** Check Inline Popover opened */\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-inline-toolbar .ce-popover__container')\n        .should('be.visible');\n\n      /** Check first item is NOT focused */\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-inline-toolbar .ce-popover__container')\n        .get('[data-item-name=\"convert-to\"].ce-popover-item--focused')\n        .should('not.exist');\n\n      /** Press Tab */\n      cy.tab();\n\n      /** Check first item became focused after tab */\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-inline-toolbar .ce-popover__container')\n        .get('[data-item-name=\"convert-to\"].ce-popover-item--focused')\n        .should('exist');\n\n      /** Check second item is NOT focused */\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-inline-toolbar .ce-popover__container')\n        .get('[data-item-name=\"link\"] .ce-popover-item--focused')\n        .should('not.exist');\n\n      /** Press Tab */\n      cy.tab();\n\n      /** Check second item became focused after tab */\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-inline-toolbar .ce-popover__container')\n        .get('[data-item-name=\"link\"] .ce-popover-item--focused')\n        .should('exist');\n    });\n\n    it('should allow to reach nested popover via keyboard', () => {\n      cy.createEditor({\n        tools: {\n          header: {\n            class: Header,\n          },\n        },\n        data: {\n          blocks: [\n            {\n              type: 'paragraph',\n              data: {\n                text: 'First block text',\n              },\n            },\n          ],\n        },\n      });\n\n      /** Open Inline Toolbar */\n      cy.get('[data-cy=editorjs]')\n        .find('.ce-paragraph')\n        .selectText('block');\n\n      /** Check Inline Popover opened */\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-inline-toolbar .ce-popover__container')\n        .should('be.visible');\n\n      /** Press Tab */\n      cy.tab();\n\n      /** Press Tab */\n      cy.get('[data-item-name=\"convert-to\"]')\n        .type('{enter}');\n\n      /** Check Inline Popover opened */\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-inline-toolbar .ce-popover--nested .ce-popover__container')\n        .should('be.visible');\n\n      /** Check first item is NOT focused */\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-popover__container')\n        .get('[data-item-name=\"header\"].ce-popover-item--focused')\n        .should('not.exist');\n\n      /** Press Tab */\n      // eslint-disable-next-line cypress/require-data-selectors -- cy.tab() not working here\n      cy.get('body').tab();\n\n      /** Check first item is focused */\n      cy.get('[data-cy=editorjs]')\n        .get('.ce-popover__container')\n        .get('[data-item-name=\"header\"].ce-popover-item--focused')\n        .should('exist');\n    });\n  });\n});\n"
  },
  {
    "path": "test/cypress/tests/utils.cy.ts",
    "content": "/* eslint-disable @typescript-eslint/no-empty-function */\nimport { isFunction } from '../../../src/components/utils';\n\n/**\n * Example of typical synchronous function\n */\nfunction syncFunction(): void {}\n\n/**\n * Example of typical asynchronous function\n */\nasync function asyncFunction(): Promise<void> {}\n\nconst syncArrowFunction = (): void => {};\n\nconst asyncArrowFunction = async (): Promise<void> => {};\n\ndescribe('isFunction function', () => {\n  it('should recognize sync functions', () => {\n    /**\n     * Act\n     */\n    const commonFunctionResult = isFunction(syncFunction);\n    const arrowFunctionResult = isFunction(syncArrowFunction);\n\n    /**\n     * Assert\n     */\n    expect(commonFunctionResult).to.eq(true);\n    expect(arrowFunctionResult).to.eq(true);\n  });\n\n  it('should recognize async functions', () => {\n    /**\n     * Act\n     */\n    const commonFunctionResult = isFunction(asyncFunction);\n    const arrowFunctionResult = isFunction(asyncArrowFunction);\n\n    /**\n     * Assert\n     */\n    expect(commonFunctionResult).to.eq(true);\n    expect(arrowFunctionResult).to.eq(true);\n  });\n\n  it('should return false if it isn\\'t a function', () => {\n    /**\n     * Arrange\n     */\n    const obj = {};\n    const num = 123;\n    const str = '123';\n\n    /**\n     * Act\n     */\n    const objResult = isFunction(obj);\n    const numResult = isFunction(num);\n    const strResult = isFunction(str);\n\n    /**\n     * Assert\n     */\n    expect(objResult).to.eq(false);\n    expect(numResult).to.eq(false);\n    expect(strResult).to.eq(false);\n  });\n});\n"
  },
  {
    "path": "test/cypress/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"compilerOptions\": {\n    \"types\": [\"cypress\", \"node\"],\n  },\n  \"include\": [\n    \"**/*.ts\"\n  ]\n}\n"
  },
  {
    "path": "test/testcases.md",
    "content": "# Editor.js specs\n\nThis document will describe various test cases of the editor.js functionality. Features will be organized by modules. Cases covered by tests should be marked by the checkmark.\n\n## Configuration\n\n- [ ] Zero configuration\n  - [ ] Editor.js should be initialized on the element with the default `editorjs` id.\n  - [ ] Editor.js should throw an error in case when there is no element with `editorjs` id.\n  - [ ] Editor.js should be initialized with the Paragraph tool only.\n  - [ ] The Inline Toolbar of the Paragraph tool should contain all default Inline Tools - `bold`, `italic`, `link`.\n\n- [ ] `holder` property\n  - [ ] Editor.js should be initialized on the element with passed via `holder` property.\n  - [ ] Editor.js should throw an error if passed `holder` value is not an Element node.\n\n- [ ] `autofocus` property\n  - [ ] With the empty editor\n    - [ ] If `true` passed, the caret should be placed to the first empty block.\n    - [ ] If `false` passed, the caret shouldn't be placed anywhere.\n    - [ ] If omitted, the caret shouldn't be placed anywhere.\n  - [ ] With the not-empty editor\n    - [ ] If `true` passed, the caret should be placed to the end of the last block.\n    - [ ] If `false` passed, the caret shouldn't be placed anywhere.\n    - [ ] If omitted, the caret shouldn't be placed anywhere.\n\n- [ ] `placeholder` property\n  - [ ] With the empty editor\n    - [ ] If `string` passed, the string should be placed as a placeholder to the first empty block only.\n    - [ ] If `false` passed, the first empty block should be placed without a placeholder.\n    - [ ] If omitted, the first empty block should be placed without a placeholder.\n\n- [ ] `minHeight` property\n  - [ ] If `number` passed, the height of the editor's bottom area from the last Block should be the `number`.\n  - [ ] If omitted the height of editor's bottom area from the last Block should be the default `300`.\n\n- [ ] `logLevel` property\n  - [ ] If `VERBOSE` passed, the editor should output all messages to the console.\n  - [ ] If `INFO` passed, the editor should output info and debug messages to the console.\n  - [ ] If `WARN` passed, the editor should output only warning messages to the console.\n  - [ ] If `ERROR` passed, the editor should output only error messages to the console.\n  - [ ] If omitted, the editor should output all messages to the console.\n\n- [ ] `defaultBlock` property\n  - [ ] If `string` passed\n    - [ ] If passed `string` in the `tools` option, the passed tool should be used as the default tool.\n    - [ ] If passed `string` not in the `tools` option, the Paragraph tool should be used as the default tool.\n  - [ ] If omitted the Paragraph tool should be used as default tool.\n\n- [ ] `sanitizer` property\n  - [ ] If `object` passed\n    - [ ] The Editor.js should clean the HTML tags according to mentioned configuration.\n  - [ ] If omitted the Editor.js should be initialized with the default `sanitizer` configuration, which allows the tags like `paragraph`, `anchor`, and `bold` for cleaning HTML.\n\n- [ ] `tools` property\n  - [ ] If omitted,the Editor.js should be initialized with the Paragraph tool only.\n  - [ ] If `object` passed\n    - [ ] Editor.js should be initialized with all the passed tools.\n    - [ ] The keys of the object should be represented as `type` fields for corresponded blocks in output JSON\n    - [ ] If value is a JavaScript class, the class should be used as a tool\n    - [ ] If value is an `object`\n      - [ ] Checking the `class` property\n        - [ ] If omitted, the tool should be skipped with a warning in a console.\n        - [ ] If existed, the value of the `class` property should be used as a tool\n      - [ ] Checking the `config` property\n        - [ ] If `object` passed Editor.js should initialize `tool` and pass this object as `config` parameter of the tool's constructor\n      - [ ] Checking the `shortcut` property\n        - [ ] If `string` passed Editor.js should append the `tool` when such keys combination executed.\n      - [ ] Checking the `inilineToolbar` property\n        - [ ] If `true` passed, the Editor.js should show the Inline Toolbar for this tool with [common](https://editorjs.io/configuration#inline-toolbar-order) settings.\n        - [ ] If `false` passed, the Editor.js should not show the Inline Toolbar for this tool.\n        - [ ] If `array` passed, the Editor.js should show the Inline Toolbar for this tool with a passed list of tools and their order.\n        - [ ] If omitted, the Editor.js should not show the Inline Toolbar for this tool.\n      - [ ] Checking the `toolbox` property\n        - [ ] If it contains `title`, this title should be used as a tool title\n        - [ ] If it contains `icon`, this HTML code (maybe SVG) should be used as a tool icon\n\n- [ ] `onReady` property\n  - [ ] If `function` passed, the Editor.js should call the `function` when it's ready to work.\n  - [ ] If omitted, the Editor.js should be initialized with the `tools` only.\n\n- [ ] `onChange` property\n  - [ ] If `function` passed,the Editor.js should call the `function` when something changed in Editor.js DOM.\n  - [ ] If omitted, the Editor.js should be initialized with the `tools` only.\n\n- [ ] `data` property\n  - [ ] If omitted\n    - [ ] the Editor.js should be initialized with the `tools` only.\n    - [ ] the Editor.js should be empty.\n  - [ ] If `object` passed\n    - [ ] Checking the `blocks` property\n      - [ ] If `array` of `object` passed,\n        - [ ] for each `object` \n          - [ ] Checking the `type` and `data` property\n            - [ ] the Editor.js should be initialize with `block` of class `type`\n            - [ ] If `type` not present in `tools`, the Editor.js should throw an error.\n      - [ ] If omitted\n        - [ ] the Editor.js should be initialized with the `tools` only.\n        - [ ] the Editor.js should be empty.\n\n- [ ] `readOnly` property\n  - [ ] If `true` passed,\n    - [ ] If any `tool` have not readOnly getter defined,The Editor.js should throw an error.\n    - [ ] otherwise, the Editor.js should be initialize with readOnly mode. \n  - [ ] If `false` passed,the Editor.js should be initialized with the `tools` only.\n  - [ ] If omitted,the Editor.js should be initialized with the `tools` only.\n\n- [ ] `i18n` property"
  },
  {
    "path": "tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"exclude\": [ \"test\" ],\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\" : {\n    \"strict\": true,\n    \"sourceMap\": true,\n    \"target\": \"es2017\",\n    \"declaration\": false,\n    \"moduleResolution\": \"node\", // This resolution strategy attempts to mimic the Node.js module resolution mechanism at runtime\n    \"lib\": [\"dom\", \"es2017\", \"es2018\", \"es2019\"],\n\n    // allows to import .json files for i18n\n    \"resolveJsonModule\": true,\n\n    // allows to omit export default in .json files\n    \"allowSyntheticDefaultImports\": true,\n    \"experimentalDecorators\": true,\n    \"baseUrl\": \"./\",\n    \"paths\": {\n      \"@/types\": [ \"./types/\" ],\n      \"@/types/*\": [ \"./types/*\" ],\n    },\n\n    // @todo move to cypress/tsconfig.json when cypress will support overriding tsconfig path\n    // @see https://github.com/cypress-io/cypress/issues/23045\n    \"esModuleInterop\": true\n  },\n}\n"
  },
  {
    "path": "tslint.json",
    "content": "{\n  \"extends\": \"tslint:recommended\",\n  \"linterOptions\": {\n    \"exclude\": [\n      \"node_modules\"\n    ]\n  },\n  \"rules\": {\n    \"indent\": [true, \"spaces\", 2],\n    \"interface-name\": false,\n    \"quotemark\": [true, \"single\"],\n    \"no-console\": false,\n    \"no-empty-interface\": false,\n    \"one-variable-per-declaration\": false,\n    \"object-literal-sort-keys\": false,\n    \"ordered-imports\": [true, {\n      \"import-sources-order\": \"any\",\n      \"named-imports-order\": \"case-insensitive\"\n    }],\n    \"no-string-literal\": false,\n    \"no-empty\": false,\n    \"no-namespace\": false,\n    \"variable-name\": [true, \"allow-leading-underscore\", \"allow-pascal-case\"],\n    \"no-reference\": false\n  },\n  \"globals\": {\n    \"require\": true\n  }\n}\n"
  },
  {
    "path": "types/api/block.d.ts",
    "content": "import {BlockToolData, ToolConfig, ToolboxConfigEntry} from '../tools';\nimport {SavedData} from '../data-formats';\n\n/**\n * @interface BlockAPI Describes Block API methods and properties\n */\nexport interface BlockAPI {\n  /**\n   * Block unique identifier\n   */\n  readonly id: string;\n\n  /**\n   * Tool name\n   */\n  readonly name: string;\n\n  /**\n   * Tool config passed on Editor's initialization\n   */\n  readonly config: ToolConfig;\n\n  /**\n   * Wrapper of Tool's HTML element\n   */\n  readonly holder: HTMLElement;\n\n  /**\n   * True if Block content is empty\n   */\n  readonly isEmpty: boolean;\n\n  /**\n   * True if Block is selected with Cross-Block selection\n   */\n  readonly selected: boolean;\n\n  /**\n   * True if Block has inputs to be focused\n   */\n  readonly focusable: boolean;\n\n  /**\n   * Setter sets Block's stretch state\n   *\n   * Getter returns true if Block is stretched\n   */\n  stretched: boolean;\n\n  /**\n   * Call Tool method with errors handler under-the-hood\n   *\n   * @param {string} methodName - method to call\n   * @param {object} param - object with parameters\n   *\n   * @return {void}\n   */\n  call(methodName: string, param?: object): void;\n\n  /**\n   * Save Block content\n   *\n   * @return {Promise<void|SavedData>}\n   */\n  save(): Promise<void|SavedData>;\n\n  /**\n   * Validate Block data\n   *\n   * @param {BlockToolData} data\n   *\n   * @return {Promise<boolean>}\n   */\n  validate(data: BlockToolData): Promise<boolean>;\n\n  /**\n   * Allows to say Editor that Block was changed. Used to manually trigger Editor's 'onChange' callback\n   * Can be useful for block changes invisible for editor core.\n   */\n  dispatchChange(): void;\n\n  /**\n   * Tool could specify several entries to be displayed at the Toolbox (for example, \"Heading 1\", \"Heading 2\", \"Heading 3\")\n   * This method returns the entry that is related to the Block (depended on the Block data)\n   */\n  getActiveToolboxEntry(): Promise<ToolboxConfigEntry | undefined>\n}\n"
  },
  {
    "path": "types/api/blocks.d.ts",
    "content": "import {OutputBlockData, OutputData} from '../data-formats/output-data';\nimport {BlockToolData, ToolConfig} from '../tools';\nimport {BlockAPI} from './block';\nimport {BlockTuneData} from '../block-tunes/block-tune-data';\n\n/**\n * Describes methods to manipulate with Editor`s blocks\n */\nexport interface Blocks {\n  /**\n   * Remove all blocks from Editor zone\n   */\n  clear(): Promise<void>;\n\n  /**\n   * Render passed data\n   *\n   * @param {OutputData} data - saved Block data\n   *\n   * @returns {Promise<void>}\n   */\n  render(data: OutputData): Promise<void>;\n\n  /**\n   * Render passed HTML string\n   * @param {string} data\n   * @return {Promise<void>}\n   */\n  renderFromHTML(data: string): Promise<void>;\n\n  /**\n   * Removes current Block\n   * @param {number} index - index of a block to delete\n   */\n  delete(index?: number): void;\n\n  /**\n   * Swaps two Blocks\n   * @param {number} fromIndex - block to swap\n   * @param {number} toIndex - block to swap with\n   * @deprecated — use 'move' instead\n   */\n  swap(fromIndex: number, toIndex: number): void;\n\n  /**\n   * Moves a block to a new index\n   * @param {number} toIndex - index where the block is moved to\n   * @param {number} fromIndex - block to move\n   */\n  move(toIndex: number, fromIndex?: number): void;\n\n  /**\n   * Returns Block API object by passed Block index\n   * @param {number} index\n   */\n  getBlockByIndex(index: number): BlockAPI | undefined;\n\n  /**\n   * Returns Block API object by passed Block id\n   * @param id - id of the block\n   */\n  getById(id: string): BlockAPI | null;\n\n  /**\n   * Returns current Block index\n   * @returns {number}\n   */\n  getCurrentBlockIndex(): number;\n\n  /**\n   * Returns the index of Block by id;\n   */\n  getBlockIndex(blockId: string): number;\n\n  /**\n   * Get Block API object by html element\n   *\n   * @param element - html element to get Block by\n   */\n  getBlockByElement(element: HTMLElement): BlockAPI | undefined;\n\n  /**\n   * Mark Block as stretched\n   * @param {number} index - Block to mark\n   * @param {boolean} status - stretch status\n   *\n   * @deprecated Use BlockAPI interface to stretch Blocks\n   */\n  stretchBlock(index: number, status?: boolean): void;\n\n  /**\n   * Returns Blocks count\n   * @return {number}\n   */\n  getBlocksCount(): number;\n\n  /**\n   * Insert new Initial Block after current Block\n   *\n   * @deprecated\n   */\n  insertNewBlock(): void;\n\n  /**\n   * Insert new Block and return inserted Block API\n   *\n   * @param {string} type — Tool name\n   * @param {BlockToolData} data — Tool data to insert\n   * @param {ToolConfig} config — Tool config\n   * @param {number?} index — index where to insert new Block\n   * @param {boolean?} needToFocus - flag to focus inserted Block\n   * @param {boolean?} replace - should the existed Block on that index be replaced or not\n   * @param {string} id — An optional id for the new block. If omitted then the new id will be generated\n   */\n  insert(\n    type?: string,\n    data?: BlockToolData,\n    config?: ToolConfig,\n    index?: number,\n    needToFocus?: boolean,\n    replace?: boolean,\n    id?: string,\n  ): BlockAPI;\n\n  /**\n   * Inserts several Blocks to specified index\n   */\n  insertMany(\n    blocks: OutputBlockData[],\n    index?: number,\n  ): BlockAPI[];\n\n\n  /**\n   * Creates data of an empty block with a passed type.\n   *\n   * @param toolName - block tool name\n   */\n  composeBlockData(toolName: string): Promise<BlockToolData>\n\n  /**\n   * Updates block data by id\n   *\n   * @param id - id of the block to update\n   * @param data - (optional) the new data. Can be partial.\n   * @param tunes - (optional) tune data\n   */\n  update(id: string, data?: Partial<BlockToolData>, tunes?: {[name: string]: BlockTuneData}): Promise<BlockAPI>;\n\n  /**\n   * Converts block to another type. Both blocks should provide the conversionConfig.\n   *\n   * @param id - id of the existed block to convert. Should provide 'conversionConfig.export' method\n   * @param newType - new block type. Should provide 'conversionConfig.import' method\n   * @param dataOverrides - optional data overrides for the new block\n   *\n   * @throws Error if conversion is not possible\n   */\n  convert(id: string, newType: string, dataOverrides?: BlockToolData): Promise<BlockAPI>;\n}\n"
  },
  {
    "path": "types/api/caret.d.ts",
    "content": "import { BlockAPI } from \"./block\";\n\n/**\n * Describes Editor`s caret API\n */\nexport interface Caret {\n\n  /**\n   * Sets caret to the first Block\n   *\n   * @param {string} position - position where to set caret\n   * @param {number} offset - caret offset\n   *\n   * @return {boolean}\n   */\n  setToFirstBlock(position?: 'end'|'start'|'default', offset?: number): boolean;\n\n  /**\n   * Sets caret to the last Block\n   *\n   * @param {string} position - position where to set caret\n   * @param {number} offset - caret offset\n   *\n   * @return {boolean}\n   */\n  setToLastBlock(position?: 'end'|'start'|'default', offset?: number): boolean;\n\n  /**\n   * Sets caret to the previous Block\n   *\n   * @param {string} position - position where to set caret\n   * @param {number} offset - caret offset\n   *\n   * @return {boolean}\n   */\n  setToPreviousBlock(position?: 'end'|'start'|'default', offset?: number): boolean;\n\n  /**\n   * Sets caret to the next Block\n   *\n   * @param {string} position - position where to set caret\n   * @param {number} offset - caret offset\n   *\n   * @return {boolean}\n   */\n  setToNextBlock(position?: 'end'|'start'|'default', offset?: number): boolean;\n\n  /**\n   * Sets caret to the Block by passed index\n   *\n   * @param blockOrIdOrIndex - BlockAPI or Block id or Block index\n   * @param position - position where to set caret\n   * @param offset - caret offset\n   *\n   * @return {boolean}\n   */\n  setToBlock(blockOrIdOrIndex: BlockAPI | BlockAPI['id'] | number, position?: 'end'|'start'|'default', offset?: number): boolean;\n\n  /**\n   * Sets caret to the Editor\n   *\n   * @param {boolean} atEnd - if true, set Caret to the end of the Editor\n   *\n   * @return {boolean}\n   */\n  focus(atEnd?: boolean): boolean;\n}\n"
  },
  {
    "path": "types/api/events.d.ts",
    "content": "/**\n * Describes Editor`s events API\n */\nexport interface Events {\n  /**\n   * Emits event\n   *\n   * @param {string} eventName\n   * @param {any} data\n   */\n  emit(eventName: string, data: any): void;\n\n  /**\n   * Unsubscribe from event\n   *\n   * @param {string} eventName\n   * @param {(data: any) => void} callback\n   */\n  off(eventName: string, callback: (data?: any) => void): void;\n\n  /**\n   * Subscribe to event\n   *\n   * @param {string} eventName\n   * @param {(data: any) => void} callback\n   */\n  on(eventName: string, callback: (data?: any) => void): void;\n}\n"
  },
  {
    "path": "types/api/i18n.d.ts",
    "content": "/**\n * Describes Editor`s I18n API\n */\nexport interface I18n {\n  /**\n   * Perform translation with automatically added namespace like `tools.${toolName}` or `blockTunes.${tuneName}`\n   *\n   * @param dictKey - what to translate\n   */\n  t(dictKey: string): string;\n}\n"
  },
  {
    "path": "types/api/index.d.ts",
    "content": "export * from './blocks';\nexport * from './events';\nexport * from './listeners';\nexport * from './sanitizer';\nexport * from './saver';\nexport * from './selection';\nexport * from './styles';\nexport * from './caret';\nexport * from './toolbar';\nexport * from './notifier';\nexport * from './tooltip';\nexport * from './inline-toolbar';\nexport * from './block';\nexport * from './readonly';\nexport * from './i18n';\nexport * from './ui';\nexport * from './tools';\n"
  },
  {
    "path": "types/api/inline-toolbar.d.ts",
    "content": "/**\n * Describes InlineToolbar API methods\n */\nexport interface InlineToolbar {\n    /**\n     * Closes InlineToolbar\n     */\n    close(): void;\n  \n    /**\n     * Opens InlineToolbar\n     */\n    open(): void;\n}\n  "
  },
  {
    "path": "types/api/listeners.d.ts",
    "content": "/**\n * Describes Editor`s listeners API\n */\nexport interface Listeners {\n  /**\n   * Subscribe to event dispatched on passed element. Returns listener id.\n   *\n   * @param {Element} element\n   * @param {string} eventType\n   * @param {(event: Event) => void}handler\n   * @param {boolean} useCapture\n   */\n  on(element: Element, eventType: string, handler: (event?: Event) => void, useCapture?: boolean): string;\n\n  /**\n   * Unsubscribe from event dispatched on passed element\n   *\n   * @param {Element} element\n   * @param {string} eventType\n   * @param {(event: Event) => void}handler\n   * @param {boolean} useCapture\n   */\n  off(element: Element, eventType: string, handler: (event?: Event) => void, useCapture?: boolean): void;\n\n\n  /**\n   * Unsubscribe from event dispatched by the listener id\n   *\n   * @param id - id of the listener to remove\n   */\n  offById(id: string): void;\n}\n"
  },
  {
    "path": "types/api/notifier.d.ts",
    "content": "import {ConfirmNotifierOptions, NotifierOptions, PromptNotifierOptions} from 'codex-notifier';\n\n/**\n * Notifier API\n */\nexport interface Notifier {\n\n  /**\n   * Show web notification\n   *\n   * @param {NotifierOptions | ConfirmNotifierOptions | PromptNotifierOptions}\n   */\n  show: (options: NotifierOptions | ConfirmNotifierOptions | PromptNotifierOptions) => void;\n}\n"
  },
  {
    "path": "types/api/readonly.d.ts",
    "content": "/**\n * ReadOnly API\n */\nexport interface ReadOnly {\n  /**\n   * Set or toggle read-only state\n   *\n   * @param {Boolean|undefined} state - set or toggle state\n   * @returns {Promise<boolean>} current value\n   */\n  toggle: (state?: boolean) => Promise<boolean>;\n\n  /**\n   * Contains current read-only state\n   */\n  isEnabled: boolean;\n}\n"
  },
  {
    "path": "types/api/sanitizer.d.ts",
    "content": "import {SanitizerConfig} from '../index';\n\n/**\n * Describes Editor`s sanitizer API\n */\nexport interface Sanitizer {\n  /**\n   * Clean taint string with html and returns clean string\n   *\n   * @param {string} taintString\n   * @param {SanitizerConfig} config - configuration for sanitizer\n   */\n  clean(taintString: string, config: SanitizerConfig): string;\n}\n"
  },
  {
    "path": "types/api/saver.d.ts",
    "content": "import {OutputData} from '../data-formats/output-data';\n\n/**\n * Describes Editor`s saver API\n */\nexport interface Saver {\n  /**\n   * Saves Editors data and returns promise with it\n   *\n   * @returns {Promise<OutputData>}\n   */\n  save(): Promise<OutputData>;\n}\n"
  },
  {
    "path": "types/api/selection.d.ts",
    "content": "/**\n * Describes methods for work with Selections\n */\nexport interface Selection {\n  /**\n   * Looks ahead from selection and find passed tag with class name\n   * @param {string} tagName - tag to find\n   * @param {string} className - tag's class name\n   * @return {HTMLElement|null}\n   */\n  findParentTag(tagName: string, className?: string): HTMLElement|null;\n\n  /**\n   * Expand selection to passed tag\n   * @param {HTMLElement} node - tag that should contain selection\n   */\n  expandToTag(node: HTMLElement): void;\n\n  /**\n   * Sets fake background.\n   * Allows to immitate selection while focus moved away\n  */\n  setFakeBackground(): void;\n  \n  /**\n   * Removes fake background\n   */\n  removeFakeBackground(): void;\n\n  /**\n   * Save selection range.\n   * Allows to save selection to be able to temporally move focus away.\n   * Might be usefull for inline tools\n   */\n  save(): void;\n\n  /**\n   * Restore saved selection range\n   */\n  restore(): void;\n}\n"
  },
  {
    "path": "types/api/styles.d.ts",
    "content": "/**\n * Describes styles API\n */\nexport interface Styles {\n  /**\n   * Main Editor`s block styles\n   */\n  block: string;\n\n  /**\n   * Styles for Inline Toolbar button\n   */\n  inlineToolButton: string;\n\n  /**\n   * Styles for active Inline Toolbar button\n   */\n  inlineToolButtonActive: string;\n\n  /**\n   * Styles for inputs\n   */\n  input: string;\n\n  /**\n   * Loader styles\n   */\n  loader: string;\n\n  /**\n   * Styles for Settings box buttons\n   */\n  settingsButton: string;\n\n  /**\n   * Styles for active Settings box buttons\n   */\n  settingsButtonActive: string;\n\n  /**\n   * Styles for buttons\n   */\n  button: string;\n}\n"
  },
  {
    "path": "types/api/toolbar.d.ts",
    "content": "/**\n * Describes Toolbar API methods\n */\nexport interface Toolbar {\n  /**\n   * Closes Toolbar\n   */\n  close(): void;\n\n  /**\n   * Opens Toolbar\n   */\n  open(): void;\n\n  /**\n   * Toggles Block Setting of the current block\n   * @param {boolean} openingState —  opening state of Block Setting\n   */\n  toggleBlockSettings(openingState?: boolean): void;\n\n  /**\n   * Toggle toolbox\n   * @param {boolean} openingState —  opening state of the toolbox\n   */\n  toggleToolbox(openingState?: boolean): void;\n}\n"
  },
  {
    "path": "types/api/tools.d.ts",
    "content": "import { BlockToolAdapter } from '../tools/adapters/block-tool-adapter';\n\n/**\n * Describes methods for accessing installed Editor tools\n */\nexport interface Tools {\n  /**\n   * Returns all available Block Tools\n   */\n  getBlockTools(): BlockToolAdapter[];\n}\n"
  },
  {
    "path": "types/api/tooltip.d.ts",
    "content": "/**\n * Tooltip API\n */\nimport {TooltipContent, TooltipOptions} from 'codex-tooltip';\n\nexport interface Tooltip {\n  /**\n   * Show tooltip\n   *\n   * @param {HTMLElement} element\n   * @param {TooltipContent} content\n   * @param {TooltipOptions} options\n   */\n  show: (element: HTMLElement, content: TooltipContent, options?: TooltipOptions) => void;\n\n  /**\n   * Hides tooltip\n   */\n  hide: () => void;\n\n  /**\n   * Decorator for showing Tooltip by mouseenter/mouseleave\n   *\n   * @param {HTMLElement} element\n   * @param {TooltipContent} content\n   * @param {TooltipOptions} options\n   */\n  onHover: (element: HTMLElement, content: TooltipContent, options?: TooltipOptions) => void;\n\n}\n"
  },
  {
    "path": "types/api/ui.d.ts",
    "content": "/**\n * Describes API module allowing to access some Editor UI elements and methods\n */\nexport interface Ui {\n  /**\n   * Allows accessing some Editor UI elements\n   */\n  nodes: UiNodes,\n}\n\n/**\n * Allows accessing some Editor UI elements\n */\nexport interface UiNodes {\n  /**\n   * Top-level editor instance wrapper\n   */\n  wrapper: HTMLElement,\n\n  /**\n   * Element that holds all the Blocks\n   */\n  redactor: HTMLElement,\n}\n"
  },
  {
    "path": "types/block-tunes/block-tune-data.d.ts",
    "content": "export type BlockTuneData = any;\n"
  },
  {
    "path": "types/block-tunes/block-tune.d.ts",
    "content": "import {API, BlockAPI, SanitizerConfig, ToolConfig} from '../index';\nimport { BlockTuneData } from './block-tune-data';\nimport { MenuConfig } from '../tools';\n\n/**\n * Describes BLockTune blueprint\n */\nexport interface BlockTune {\n  /**\n   * Returns BlockTune's UI.\n   * Should return either MenuConfig (recommended) (@see https://editorjs.io/menu-config/)\n   * or HTMLElement (UI consitency is not guaranteed)\n   */\n  render(): HTMLElement | MenuConfig;\n\n  /**\n   * Method called on Tool render. Pass Tool content as an argument.\n   *\n   * You can wrap Tool's content with any wrapper you want to provide Tune's UI\n   *\n   * @param {HTMLElement} pluginsContent — Tool's content wrapper\n   */\n  wrap?(pluginsContent: HTMLElement): HTMLElement;\n\n  /**\n   * Called on Tool's saving. Should return any data Tune needs to save\n   *\n   * @return {BlockTuneData}\n   */\n  save?(): BlockTuneData;\n}\n\n/**\n * Describes BlockTune class constructor function\n */\nexport interface BlockTuneConstructable {\n\n  /**\n   * Flag show Tool is Block Tune\n   */\n  isTune: boolean;\n\n  /**\n   * Tune's sanitize configuration\n   */\n  sanitize?: SanitizerConfig;\n\n  /**\n   * @constructor\n   *\n   * @param config - Block Tune config\n   */\n  new(config: {\n    api: API,\n    config?: ToolConfig,\n    block: BlockAPI,\n    data: BlockTuneData,\n  }): BlockTune;\n\n  /**\n   * Tune`s prepare method. Can be async\n   * @param data\n   */\n  prepare?(): Promise<void> | void;\n\n  /**\n   * Tune`s reset method to clean up anything set by prepare. Can be async\n   */\n  reset?(): void | Promise<void>;\n}\n"
  },
  {
    "path": "types/block-tunes/index.d.ts",
    "content": "export * from './block-tune';\n"
  },
  {
    "path": "types/configs/conversion-config.ts",
    "content": "import type { BlockToolData, ToolConfig } from '../tools';\n\n/**\n * Config allows Tool to specify how it can be converted into/from another Tool\n */\nexport interface ConversionConfig {\n  /**\n   * How to import string to this Tool.\n   *\n   * Can be a String or Function:\n   *\n   * 1. String — the key of Tool data object to fill it with imported string on render.\n   * 2. Function — method that accepts importing string and composes Tool data to render.\n   */\n  import?: ((data: string, config: ToolConfig) => BlockToolData) | string;\n\n  /**\n   * How to export this Tool to make other Block.\n   *\n   * Can be a String or Function:\n   *\n   * 1. String — which property of saved Tool data should be used as exported string.\n   * 2. Function — accepts saved Tool data and create a string to export\n   */\n  export?: ((data: BlockToolData) => string) | string;\n}\n"
  },
  {
    "path": "types/configs/editor-config.d.ts",
    "content": "import {ToolConstructable, ToolSettings} from '../tools';\nimport {API, LogLevels, OutputData} from '../index';\nimport {SanitizerConfig} from './sanitizer-config';\nimport {I18nConfig} from './i18n-config';\nimport { BlockMutationEvent } from '../events/block';\n\nexport interface EditorConfig {\n  /**\n   * Element where Editor will be append\n   * @deprecated property will be removed in the next major release, use holder instead\n   */\n  holderId?: string | HTMLElement;\n\n  /**\n   * Element where Editor will be appended\n   */\n  holder?: string | HTMLElement;\n\n  /**\n   * If true, set caret at the first Block after Editor is ready\n   */\n  autofocus?: boolean;\n\n  /**\n   * This Tool will be used as default\n   * Name should be equal to one of Tool`s keys of passed tools\n   * If not specified, Paragraph Tool will be used\n   */\n  defaultBlock?: string;\n\n  /**\n   * @deprecated\n   * This property will be deprecated in the next major release.\n   * Use the 'defaultBlock' property instead.\n   */\n  initialBlock?: string;\n\n  /**\n   * First Block placeholder\n   */\n  placeholder?: string|false;\n\n  /**\n   * Define default sanitizer configuration\n   * @see {@link sanitizer}\n   */\n  sanitizer?: SanitizerConfig;\n\n  /**\n   * If true, toolbar won't be shown\n   */\n  hideToolbar?: boolean;\n\n  /**\n   * Map of Tools to use\n   */\n  tools?: {\n    [toolName: string]: ToolConstructable|ToolSettings;\n  }\n\n  /**\n   * Data to render on Editor start\n   */\n  data?: OutputData;\n\n  /**\n   * Height of Editor's bottom area that allows to set focus on the last Block\n   */\n  minHeight?: number;\n\n  /**\n   * Editors log level (how many logs you want to see)\n   */\n  logLevel?: LogLevels;\n\n  /**\n   * Enable read-only mode\n   */\n  readOnly?: boolean;\n\n  /**\n   * Internalization config\n   */\n  i18n?: I18nConfig;\n\n  /**\n   * Fires when Editor is ready to work\n   */\n  onReady?(): void;\n\n  /**\n   * Fires when something changed in DOM\n   * @param api - editor.js api\n   * @param event - custom event describing mutation. If several mutations happened at once, they will be batched and you'll get an array of events here.\n   */\n  onChange?(api: API, event: BlockMutationEvent | BlockMutationEvent[]): void;\n\n  /**\n   * Defines default toolbar for all tools.\n   */\n  inlineToolbar?: string[]|boolean;\n\n  /**\n   * Common Block Tunes list. Will be added to all the blocks which do not specify their own 'tunes' set\n   */\n  tunes?: string[];\n\n  /**\n   * Section for style-related settings\n   */\n  style?: {\n    /**\n     * A random value to handle Content Security Policy \"style-src\" policy\n     * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce\n     */\n    nonce?: string;\n  }\n}\n"
  },
  {
    "path": "types/configs/i18n-config.d.ts",
    "content": "/**\n * Available options of i18n config property\n */\nimport { I18nDictionary } from './i18n-dictionary';\n\nexport interface I18nConfig {\n  /**\n   * Dictionary used for translation\n   */\n  messages?: I18nDictionary;\n\n  /**\n   * Text direction. If not set, uses ltr\n   */\n  direction?: 'ltr' | 'rtl';\n}\n"
  },
  {
    "path": "types/configs/i18n-dictionary.d.ts",
    "content": "/**\n * Structure of the i18n dictionary\n */\nexport interface I18nDictionary {\n  /**\n   * Section for translation Tool Names: both block and inline tools\n   * Example:\n   *  \"toolNames\": {\n   *     \"Text\": \"Параграф\",\n   *     \"Heading\": \"Заголовок\",\n   *     \"List\": \"Список\",\n   *     ...\n   *  },\n   */\n  toolNames?: Dictionary;\n\n  /**\n   * Section for passing translations to the external tools classes\n   * The first-level keys of this object should be equal of keys ot the 'tools' property of EditorConfig\n   * Includes internal tools: \"paragraph\", \"stub\"\n   *\n   * Example:\n   *  \"tools\": {\n   *     \"warning\": {\n   *       \"Title\": \"Название\",\n   *       \"Message\": \"Сообщение\",\n   *     },\n   *     \"link\": {\n   *        \"Add a link\": \"Вставьте ссылку\"\n   *     },\n   *  },\n   */\n  tools?: Dictionary;\n\n  /**\n   * Section allows to translate Block Tunes\n   * The first-level keys of this object should be equal of 'name' ot the 'tools.<toolName>.tunes' property of EditorConfig\n   * Including some internal block-tunes: \"delete\", \"moveUp\", \"moveDown\n   *\n   * Example:\n   * \"blockTunes\": {\n   *   \"delete\": {\n   *     \"Delete\": \"Удалить\"\n   *   },\n   *   \"moveUp\": {\n   *     \"Move up\": \"Переместить вверх\"\n   *   },\n   *   \"moveDown\": {\n   *     \"Move down\": \"Переместить вниз\"\n   *   }\n   * },\n   */\n  blockTunes?: Dictionary;\n\n  /**\n   * Translation of internal UI components of the editor.js core\n   */\n  ui?: Dictionary;\n}\n\n/**\n * Represent item of the I18nDictionary config\n */\nexport interface Dictionary {\n  /**\n   * The keys of the object can represent two entities:\n   *  1. Dictionary key usually is an original string from default locale, like \"Convert to\"\n   *  2. Sub-namespace section, like \"toolbar.converter.<...>\"\n   *\n   *  Example of 1:\n   *  toolbox: {\n   *    \"Add\": \"Добавить\",\n   *  }\n   *\n   *  Example of 2:\n   *  ui: {\n   *    toolbar: {\n   *      toolbox: {    <-- Example of 1\n   *        \"Add\": \"Добавить\"\n   *      }\n   *    }\n   *  }\n   */\n  [key: string]: DictValue;\n}\n\n/**\n * The value of the dictionary can be:\n *  - other dictionary\n *  - result translate string\n */\nexport type DictValue = {[key: string]: Dictionary | string} | string;\n\n"
  },
  {
    "path": "types/configs/index.d.ts",
    "content": "export * from './editor-config';\nexport * from './sanitizer-config';\nexport * from './paste-config';\nexport * from './conversion-config';\nexport * from './log-levels';\nexport * from './i18n-config';\nexport * from './i18n-dictionary';\n"
  },
  {
    "path": "types/configs/log-levels.d.ts",
    "content": "/**\n * Available log levels\n */\nexport enum LogLevels {\n  VERBOSE = 'VERBOSE',\n  INFO = 'INFO',\n  WARN = 'WARN',\n  ERROR = 'ERROR',\n}\n"
  },
  {
    "path": "types/configs/paste-config.d.ts",
    "content": "import { SanitizerConfig } from './sanitizer-config';\n\n/**\n * Tool onPaste configuration object\n */\ninterface PasteConfigSpecified {\n  /**\n   * Array of tags Tool can substitute.\n   *\n   * Could also contain a sanitize-config if you need to save some tag's attribute.\n   * For example:\n   * [\n   *   {\n   *     img: { src: true },\n   *   }\n   * ],\n   * @type string[]\n   */\n  tags?: (string | SanitizerConfig)[];\n\n  /**\n   * Object of string patterns Tool can substitute.\n   * Key is your internal key and value is RegExp\n   *\n   * @type {{[key: string]: RegExp}}\n   */\n  patterns?: {[key: string]: RegExp};\n\n  /**\n   * Object with arrays of extensions and MIME types Tool can substitute\n   */\n  files?: {extensions?: string[], mimeTypes?: string[]};\n}\n\n/**\n * Alias for PasteConfig with false\n */\nexport type PasteConfig = PasteConfigSpecified | false;\n"
  },
  {
    "path": "types/configs/sanitizer-config.d.ts",
    "content": "/**\n * Sanitizer config of each HTML element\n * @see {@link https://github.com/guardian/html-janitor#options}\n */\nexport type TagConfig = boolean | { [attr: string]: boolean | string };\n\nexport type SanitizerRule = TagConfig | ((el: Element) => TagConfig)\n\nexport interface SanitizerConfig {\n  /**\n   * Tag name and params not to be stripped off\n   * @see {@link https://github.com/guardian/html-janitor}\n   *\n   * @example Save P tags\n   * p: true\n   *\n   * @example Save A tags and do not strip HREF attribute\n   * a: {\n   *   href: true\n   * }\n   *\n   * @example Save A tags with TARGET=\"_blank\" attribute\n   * a: function (aTag) {\n   *   return aTag.target === '_black';\n   * }\n   *\n   * @example Save U tags that are not empty\n   * u: function(el){\n   *   return el.textContent !== '';\n   * }\n   *\n   * @example For blockquote with class 'indent' save CLASS and STYLE attributes\n   *          Otherwise strip all attributes\n   * blockquote: function(el) {\n   *   if (el.classList.contains('indent')) {\n   *     return { 'class': true, 'style': true };\n   *   } else {\n   *     return {};\n   *   }\n   * }\n   */\n  [key: string]: SanitizerRule;\n}\n"
  },
  {
    "path": "types/data-formats/block-data.d.ts",
    "content": "import {BlockToolData} from '../tools';\nimport { BlockId } from './block-id';\n\n/**\n * Tool's saved data\n */\nexport interface SavedData {\n  id: BlockId;\n  tool: string;\n  data: BlockToolData;\n  time: number;\n}\n\n/**\n * Tool's data after validation\n */\nexport interface ValidatedData {\n  id?: BlockId;\n  tool?: string;\n  data?: BlockToolData;\n  time?: number;\n  isValid: boolean;\n}\n"
  },
  {
    "path": "types/data-formats/block-id.ts",
    "content": "/**\n * Unique identifier of a block\n */\nexport type BlockId = string;\n"
  },
  {
    "path": "types/data-formats/index.d.ts",
    "content": "export * from './block-data';\nexport * from './output-data';\n"
  },
  {
    "path": "types/data-formats/output-data.d.ts",
    "content": "import {BlockToolData} from '../tools';\nimport {BlockTuneData} from '../block-tunes/block-tune-data';\nimport { BlockId } from './block-id';\n\n/**\n * Output of one Tool\n *\n * @template Type - the string literal describing a tool type\n * @template Data - the structure describing a data object supported by the tool\n */\nexport interface OutputBlockData<Type extends string = string, Data extends object = any> {\n  /**\n   * Unique Id of the block\n   */\n  id?: BlockId;\n  /**\n   * Tool type\n   */\n  type: Type;\n  /**\n   * Saved Block data\n   */\n  data: BlockToolData<Data>;\n\n  /**\n   * Block Tunes data\n   */\n  tunes?: {[name: string]: BlockTuneData};\n}\n\nexport interface OutputData {\n  /**\n   * Editor's version\n   */\n  version?: string;\n\n  /**\n   * Timestamp of saving in milliseconds\n   */\n  time?: number;\n\n  /**\n   * Saved Blocks\n   */\n  blocks: OutputBlockData[];\n}\n"
  },
  {
    "path": "types/events/block/Base.ts",
    "content": "import type { BlockAPI } from '../../api';\n\n/**\n * Details of CustomEvent fired on block mutation\n */\nexport interface BlockMutationEventDetail {\n  /**\n   * Affected block\n   */\n  target: BlockAPI;\n}\n"
  },
  {
    "path": "types/events/block/BlockAdded.ts",
    "content": "import type { BlockMutationEventDetail } from './Base';\n\n/**\n * Type name of CustomEvent related to block added event\n */\nexport const BlockAddedMutationType = 'block-added';\n\n/**\n * Information about added block\n */\ninterface BlockAddedEventDetail extends BlockMutationEventDetail {\n  /**\n   * Index of added block\n   */\n  index: number;\n}\n\n/**\n * Event will be fired when the new block is added to the editor\n */\nexport type BlockAddedEvent = CustomEvent<BlockAddedEventDetail>;\n"
  },
  {
    "path": "types/events/block/BlockChanged.ts",
    "content": "import type { BlockMutationEventDetail } from './Base';\n\n/**\n * Type name of CustomEvent related to block changed event\n */\nexport const BlockChangedMutationType = 'block-changed';\n\n/**\n * Information about changed block\n */\ninterface BlockChangedEventDetail extends BlockMutationEventDetail {\n  /**\n   * Index of changed block\n   */\n  index: number;\n}\n\n/**\n * Event will be fired when some block is changed\n */\nexport type BlockChangedEvent = CustomEvent<BlockChangedEventDetail>;\n"
  },
  {
    "path": "types/events/block/BlockMoved.ts",
    "content": "import type { BlockMutationEventDetail } from './Base';\n\n/**\n * Type name of CustomEvent related to block moved event\n */\nexport const BlockMovedMutationType = 'block-moved';\n\n/**\n * Information about moved block\n */\ninterface BlockMovedEventDetail extends BlockMutationEventDetail {\n  /**\n   * Previous block position\n   */\n  fromIndex: number;\n\n  /**\n   * New block position\n   */\n  toIndex: number;\n}\n\n/**\n * Event will be fired when some block is moved to another position\n */\nexport type BlockMovedEvent = CustomEvent<BlockMovedEventDetail>;\n"
  },
  {
    "path": "types/events/block/BlockRemoved.ts",
    "content": "import type { BlockMutationEventDetail } from './Base';\n\n/**\n * Type name of CustomEvent related to block removed event\n */\nexport const BlockRemovedMutationType = 'block-removed';\n\n/**\n * Information about removed block\n */\ninterface BlockRemovedEventDetail extends BlockMutationEventDetail {\n  /**\n   * Index of removed block\n   */\n  index: number;\n}\n\n/**\n * Event will be fired when some block is removed\n */\nexport type BlockRemovedEvent = CustomEvent<BlockRemovedEventDetail>;\n"
  },
  {
    "path": "types/events/block/index.ts",
    "content": "import type { BlockAddedEvent, BlockAddedMutationType } from './BlockAdded';\nimport type { BlockChangedEvent, BlockChangedMutationType } from './BlockChanged';\nimport type { BlockMovedEvent, BlockMovedMutationType } from './BlockMoved';\nimport type { BlockRemovedEvent, BlockRemovedMutationType } from './BlockRemoved';\n\n/**\n * Map for Custom Events related to block mutation types\n */\nexport interface BlockMutationEventMap {\n  /**\n   * New Block added\n   */\n  [BlockAddedMutationType]: BlockAddedEvent;\n\n  /**\n   * On Block deletion\n   */\n  [BlockRemovedMutationType]: BlockRemovedEvent;\n\n  /**\n   * Moving of a Block\n   */\n  [BlockMovedMutationType]: BlockMovedEvent;\n\n  /**\n   * Any changes inside the Block\n   */\n  [BlockChangedMutationType]: BlockChangedEvent;\n}\n\n/**\n * What kind of modification happened with the Block\n */\nexport type BlockMutationType = keyof BlockMutationEventMap;\n\n/**\n * Returns a union type of values of passed object\n */\ntype ValueOf<T> = T[keyof T];\n\n/**\n * CustomEvent describing a change related to a block\n */\nexport type BlockMutationEvent = ValueOf<BlockMutationEventMap>;\n"
  },
  {
    "path": "types/index.d.ts",
    "content": "/**\n * For export type there should be one entry point,\n * so we export all types from this file\n * ------------------------------------\n */\n\nimport {\n  Dictionary,\n  DictValue,\n  EditorConfig,\n  I18nConfig,\n  I18nDictionary,\n} from './configs';\n\nimport {\n  Blocks,\n  Caret,\n  Events,\n  InlineToolbar,\n  Listeners,\n  Notifier,\n  ReadOnly,\n  Sanitizer,\n  Saver,\n  Selection,\n  Styles,\n  Toolbar,\n  Tooltip,\n  I18n,\n  Ui,\n  Tools,\n} from './api';\n\nimport { OutputData } from './data-formats';\nimport { BlockMutationEvent, BlockMutationEventMap, BlockMutationType } from './events/block';\nimport { BlockAddedMutationType, BlockAddedEvent } from './events/block/BlockAdded';\nimport { BlockChangedMutationType, BlockChangedEvent } from './events/block/BlockChanged';\nimport { BlockMovedMutationType, BlockMovedEvent } from './events/block/BlockMoved';\nimport { BlockRemovedMutationType, BlockRemovedEvent } from './events/block/BlockRemoved';\n\n/**\n * Interfaces used for development\n */\nexport {\n  BaseTool,\n  BaseToolConstructable,\n  InlineTool,\n  InlineToolConstructable,\n  InlineToolConstructorOptions,\n  BlockToolConstructable,\n  BlockToolConstructorOptions,\n  BlockTool,\n  BlockToolData,\n  Tool,\n  ToolConstructable,\n  ToolboxConfig,\n  ToolboxConfigEntry,\n  ToolSettings,\n  ToolConfig,\n  PasteEvent,\n  PasteEventDetail,\n  PatternPasteEvent,\n  PatternPasteEventDetail,\n  HTMLPasteEvent,\n  HTMLPasteEventDetail,\n  FilePasteEvent,\n  FilePasteEventDetail,\n} from './tools';\nexport {BlockTune, BlockTuneConstructable} from './block-tunes';\nexport {\n  EditorConfig,\n  SanitizerConfig,\n  SanitizerRule,\n  PasteConfig,\n  LogLevels,\n  ConversionConfig,\n  I18nDictionary,\n  Dictionary,\n  DictValue,\n  I18nConfig,\n} from './configs';\n\nexport * from './utils/popover';\n\nexport { OutputData, OutputBlockData} from './data-formats/output-data';\nexport { BlockId } from './data-formats/block-id';\nexport { BlockAPI } from './api'\nexport {\n  BlockMutationType,\n  BlockMutationEvent,\n  BlockMutationEventMap,\n  BlockAddedMutationType,\n  BlockAddedEvent,\n  BlockRemovedMutationType,\n  BlockRemovedEvent,\n  BlockMovedMutationType,\n  BlockMovedEvent,\n  BlockChangedMutationType,\n  BlockChangedEvent,\n}\n\n/**\n * We have a namespace API {@link ./api/index.d.ts} (APIMethods) but we can not use it as interface\n * So we should create new interface for exporting API type\n */\nexport interface API {\n  blocks: Blocks;\n  caret: Caret;\n  tools: Tools;\n  events: Events;\n  listeners: Listeners;\n  notifier: Notifier;\n  sanitizer: Sanitizer;\n  saver: Saver;\n  selection: Selection;\n  styles: Styles;\n  toolbar: Toolbar;\n  inlineToolbar: InlineToolbar;\n  tooltip: Tooltip;\n  i18n: I18n;\n  readOnly: ReadOnly;\n  ui: Ui;\n}\n\n/**\n * Main Editor class\n */\ndeclare class EditorJS {\n  public static version: string;\n\n  public isReady: Promise<void>;\n\n  public blocks: Blocks;\n  public caret: Caret;\n  public sanitizer: Sanitizer;\n  public saver: Saver;\n  public selection: Selection;\n  public styles: Styles;\n  public toolbar: Toolbar;\n  public inlineToolbar: InlineToolbar;\n  public readOnly: ReadOnly;\n  constructor(configuration?: EditorConfig|string);\n\n  /**\n   * API shorthands\n   */\n\n  /**\n   * @see Saver.save\n   */\n  public save(): Promise<OutputData>;\n\n  /**\n   * @see Blocks.clear\n   */\n  public clear(): void;\n\n  /**\n   * @see Blocks.render\n   */\n  public render(data: OutputData): Promise<void>;\n\n  /**\n   * @see Caret.focus\n   */\n  public focus(atEnd?: boolean): boolean;\n\n  /**\n   * @see Events.on\n   */\n  public on(eventName: string, callback: (data?: any) => void): void;\n\n  /**\n   * @see Events.off\n   */\n  public off(eventName: string, callback: (data?: any) => void): void;\n\n  /**\n   * @see Events.emit\n   */\n  public emit(eventName: string, data: any): void;\n\n  /**\n   * Destroy Editor instance and related DOM elements\n   */\n  public destroy(): void;\n}\n\nexport as namespace EditorJS;\nexport default EditorJS;\n"
  },
  {
    "path": "types/tools/adapters/base-tool-adapter.d.ts",
    "content": "import { Tool, ToolSettings } from '..';\nimport type { SanitizerConfig } from '../..';\n\nimport { ToolType } from './tool-type';\nimport { InlineToolAdapter } from './inline-tool-adapter';\nimport { BlockToolAdapter } from './block-tool-adapter';\nimport { BlockTuneAdapter } from './block-tune-adapter';\n\nexport interface BaseToolAdapter<Type extends ToolType, ToolClass extends Tool> {\n  /**\n   * Tool type: Block, Inline or Tune\n   */\n  type: Type;\n\n  /**\n   * Tool name specified in EditorJS config\n   */\n  name: string;\n\n  /**\n   * Flag show is current Tool internal (bundled with EditorJS core) or not\n   */\n  readonly isInternal: boolean;\n\n  /**\n   * Flag show is current Tool default or not\n   */\n  readonly isDefault: boolean;\n\n  /**\n   * Returns Tool user configuration\n   */\n  settings: ToolSettings;\n\n  /**\n   * Calls Tool's reset method\n   */\n  reset(): void | Promise<void>;\n\n  /**\n   * Calls Tool's prepare method\n   */\n  prepare(): void | Promise<void>;\n\n  /**\n   * Returns shortcut for Tool (internal or specified by user)\n   */\n  shortcut: string | undefined;\n\n  /**\n   * Returns Tool's sanitizer configuration\n   */\n  sanitizeConfig: SanitizerConfig;\n\n  /**\n   * Returns true if Tools is inline\n   */\n  isInline(): this is InlineToolAdapter;\n\n  /**\n   * Returns true if Tools is block\n   */\n  isBlock(): this is BlockToolAdapter;\n\n  /**\n   * Returns true if Tools is tune\n   */\n  isTune(): this is BlockTuneAdapter;\n\n  /**\n   * Constructs new Tool instance from constructable blueprint\n   *\n   * @param args\n   */\n  create(...args: any[]): ToolClass;\n}\n"
  },
  {
    "path": "types/tools/adapters/block-tool-adapter.d.ts",
    "content": "import { ToolsCollection } from './tools-collection';\nimport { ToolType } from './tool-type';\nimport { InlineToolAdapter } from './inline-tool-adapter';\nimport { BlockTuneAdapter } from './block-tune-adapter';\nimport { BlockTool } from '../block-tool';\nimport { BlockToolData } from '../block-tool-data';\nimport { BlockAPI } from '../../api/block';\nimport { ToolboxConfigEntry } from '../tool-settings';\nimport { ConversionConfig } from '../../configs/conversion-config';\nimport { PasteConfig } from '../../configs/paste-config';\nimport { SanitizerConfig } from '../../configs/sanitizer-config';\nimport { BaseToolAdapter } from './base-tool-adapter';\n\ninterface BlockToolAdapter extends BaseToolAdapter<ToolType.Block, BlockTool>{\n  /**\n   * InlineTool collection for current Block Tool\n   */\n  inlineTools: ToolsCollection<InlineToolAdapter>;\n\n  /**\n   * BlockTune collection for current Block Tool\n   */\n  tunes: ToolsCollection<BlockTuneAdapter>;\n\n  /**\n   * Creates new Tool instance\n   * @param data - Tool data\n   * @param block - BlockAPI for current Block\n   * @param readOnly - True if Editor is in read-only mode\n   */\n  create(data: BlockToolData, block: BlockAPI, readOnly: boolean): BlockTool;\n\n  /**\n   * Returns true if read-only mode is supported by Tool\n   */\n  isReadOnlySupported: boolean;\n\n  /**\n   * Returns true if Tool supports linebreaks\n   */\n  isLineBreaksEnabled: boolean;\n\n  /**\n   * Returns Tool toolbox configuration (internal or user-specified)\n   */\n  toolbox: ToolboxConfigEntry[] | undefined;\n\n  /**\n   * Returns Tool conversion configuration\n   */\n  conversionConfig: ConversionConfig | undefined;\n\n  /**\n   * Returns enabled inline tools for Tool\n   */\n  enabledInlineTools: boolean | string[];\n\n  /**\n   * Returns enabled tunes for Tool\n   */\n  enabledBlockTunes: boolean | string[];\n\n  /**\n   * Returns Tool paste configuration\n   */\n  pasteConfig: PasteConfig;\n\n  /**\n   * Returns sanitize configuration for Block Tool including configs from related Inline Tools and Block Tunes\n   */\n  sanitizeConfig: SanitizerConfig;\n\n  /**\n   * Returns sanitizer configuration composed from sanitize config of Inline Tools enabled for Tool\n   */\n  baseSanitizeConfig: SanitizerConfig;\n}\n\n"
  },
  {
    "path": "types/tools/adapters/block-tune-adapter.d.ts",
    "content": "import { BlockAPI, BlockTune } from '../..';\nimport { BlockTuneData } from '../../block-tunes/block-tune-data';\nimport { BaseToolAdapter } from './base-tool-adapter';\nimport { ToolType } from './tool-type';\n\ninterface BlockTuneAdapter extends BaseToolAdapter<ToolType.Tune, BlockTune> {\n  /**\n   * Constructs new BlockTune instance from constructable\n   *\n   * @param data - Tune data\n   * @param block - Block API object\n   */\n  create(data: BlockTuneData, block: BlockAPI): BlockTune;\n}\n"
  },
  {
    "path": "types/tools/adapters/inline-tool-adapter.d.ts",
    "content": "import { InlineTool as IInlineTool, InlineTool } from '../..';\nimport { BaseToolAdapter } from './base-tool-adapter';\nimport { ToolType } from './tool-type';\n\ninterface InlineToolAdapter extends BaseToolAdapter<ToolType.Inline, InlineTool> {\n  /**\n   * Returns title for Inline Tool if specified by user\n   */\n  title: string;\n\n  /**\n   * Constructs new InlineTool instance from constructable\n   */\n  create(): IInlineTool;\n}\n"
  },
  {
    "path": "types/tools/adapters/tool-factory.d.ts",
    "content": "import { BlockToolAdapter } from './block-tool-adapter';\nimport { BlockTuneAdapter } from './block-tune-adapter';\nimport { InlineToolAdapter } from './inline-tool-adapter';\n\nexport type ToolFactory = BlockToolAdapter | InlineToolAdapter | BlockTuneAdapter;\n"
  },
  {
    "path": "types/tools/adapters/tool-type.ts",
    "content": "/**\n * What kind of plugins developers can create\n */\nexport enum ToolType {\n  /**\n   * Block tool\n   */\n  Block,\n  /**\n   * Inline tool\n   */\n  Inline,\n\n  /**\n   * Block tune\n   */\n  Tune,\n}\n"
  },
  {
    "path": "types/tools/adapters/tools-collection.d.ts",
    "content": "import { BlockToolAdapter } from './block-tool-adapter';\nimport { BlockTuneAdapter } from './block-tune-adapter';\nimport { InlineToolAdapter } from './inline-tool-adapter';\nimport { ToolFactory } from './tool-factory';\n\n/**\n * Interface for a collection of tools.\n */\nexport interface ToolsCollection<V extends ToolFactory = ToolFactory> {\n  /**\n   * Returns Block Tools collection\n   */\n  blockTools: ToolsCollection<BlockToolAdapter>;\n\n  /**\n   * Returns Inline Tools collection\n   */\n  inlineTools: ToolsCollection<InlineToolAdapter>;\n\n  /**\n   * Returns Block Tunes collection\n   */\n  blockTunes: ToolsCollection<BlockTuneAdapter>;\n\n  /**\n   * Returns internal Tools collection\n   */\n  internalTools: ToolsCollection<V>;\n\n  /**\n   * Returns Tools collection provided by user\n   */\n  externalTools: ToolsCollection<V>;\n}\n"
  },
  {
    "path": "types/tools/block-tool-data.d.ts",
    "content": "/**\n * Object returned by Tool's {@link BlockTool#save} method\n * Specified by Tool developer, so leave it as object\n */\nexport type BlockToolData<T extends object = any> = T;\n"
  },
  {
    "path": "types/tools/block-tool.d.ts",
    "content": "import { ConversionConfig, PasteConfig, SanitizerConfig } from '../configs';\nimport { BlockToolData } from './block-tool-data';\nimport { BaseTool, BaseToolConstructable, BaseToolConstructorOptions } from './tool';\nimport { ToolConfig } from './tool-config';\nimport { API, BlockAPI, ToolboxConfig } from '../index';\nimport { PasteEvent } from './paste-events';\nimport { MoveEvent } from './hook-events';\nimport { MenuConfig } from './menu-config';\n\n/**\n * Describe Block Tool object\n * @see {@link docs/tools.md}\n */\nexport interface BlockTool extends BaseTool {\n  /**\n   * Sanitizer rules description\n   */\n  sanitize?: SanitizerConfig;\n\n  /**\n   * Process Tool's element in DOM and return raw data\n   * @param {HTMLElement} block - element created by {@link BlockTool#render} function\n   * @return {BlockToolData}\n   */\n  save(block: HTMLElement): BlockToolData;\n\n  /**\n   * Create Block's settings block\n   */\n  renderSettings?(): HTMLElement | MenuConfig;\n\n  /**\n   * Validate Block's data\n   * @param {BlockToolData} blockData\n   * @return {boolean}\n   */\n  validate?(blockData: BlockToolData): boolean;\n\n  /**\n   * Method that specified how to merge two Blocks with same type.\n   * Called by backspace at the beginning of the Block\n   * @param {BlockToolData} blockData\n   */\n  merge?(blockData: BlockToolData): void;\n\n  /**\n   * On paste callback. Fired when pasted content can be substituted by a Tool\n   * @param {PasteEvent} event\n   */\n  onPaste?(event: PasteEvent): void;\n\n  /**\n   * Cleanup resources used by your tool here\n   * Called when the editor is destroyed\n   */\n  destroy?(): void;\n\n  /**\n   * Lifecycle hooks\n   */\n\n  /**\n   * Called after block content added to the page\n   */\n  rendered?(): void;\n\n  /**\n   * Called each time block content is updated\n   */\n  updated?(): void;\n\n  /**\n   * Called after block removed from the page but before instance is deleted\n   */\n  removed?(): void;\n\n  /**\n   * Called after block was moved\n   */\n  moved?(event: MoveEvent): void;\n}\n\n/**\n * Describe constructor parameters\n */\nexport interface BlockToolConstructorOptions<D extends object = any, C extends object = any> extends BaseToolConstructorOptions<C> {\n  data: BlockToolData<D>;\n  block: BlockAPI;\n  readOnly: boolean;\n}\n\nexport interface BlockToolConstructable extends BaseToolConstructable {\n  /**\n   * Tool's Toolbox settings\n   */\n  toolbox?: ToolboxConfig;\n\n  /**\n   * Paste substitutions configuration\n   */\n  pasteConfig?: PasteConfig | false;\n\n  /**\n   * Rules that specified how this Tool can be converted into/from another Tool\n   */\n  conversionConfig?: ConversionConfig;\n\n  /**\n   * Is Tool supports read-only mode, this property should return true\n   */\n  isReadOnlySupported?: boolean;\n\n  /**\n   * @constructor\n   *\n   * @param {BlockToolConstructorOptions} config - constructor parameters\n   *\n   * @return {BlockTool}\n   */\n  new(config: BlockToolConstructorOptions): BlockTool;\n}\n"
  },
  {
    "path": "types/tools/hook-events.d.ts",
    "content": "/**\n * Event detail for block relocation\n */\nexport interface MoveEventDetail {\n  /**\n   * index the block was moved from\n   */\n  fromIndex: number;\n  /**\n   * index the block was moved to\n   */\n  toIndex: number;\n}\n\n/**\n * Move event for block relocation\n */\nexport interface MoveEvent extends CustomEvent {\n  /**\n   * Override detail property of CustomEvent by MoveEvent hook\n   */\n  readonly detail: MoveEventDetail;\n}\n"
  },
  {
    "path": "types/tools/index.d.ts",
    "content": "import { BlockTool, BlockToolConstructable } from './block-tool';\nimport { InlineTool, InlineToolConstructable } from './inline-tool';\nimport { BlockTune, BlockTuneConstructable } from '../block-tunes';\n\nexport * from './block-tool';\nexport * from './block-tool-data';\nexport * from './inline-tool';\nexport * from './tool';\nexport * from './tool-config';\nexport * from './tool-settings';\nexport * from './paste-events';\nexport * from './hook-events';\nexport * from './menu-config';\n\nexport type Tool = BlockTool | InlineTool | BlockTune;\nexport type ToolConstructable = BlockToolConstructable | InlineToolConstructable | BlockTuneConstructable;\n"
  },
  {
    "path": "types/tools/inline-tool.d.ts",
    "content": "import {BaseTool, BaseToolConstructable} from './tool';\nimport {API, ToolConfig} from '../index';\nimport { MenuConfig } from './menu-config';\n/**\n * Base structure for the Inline Toolbar Tool\n */\nexport interface InlineTool extends BaseTool<HTMLElement | MenuConfig> {\n  /**\n   * Shortcut for Tool\n   * @type {string}\n   */\n  shortcut?: string;\n\n  /**\n   * Method that accepts selected range and wrap it somehow\n   * @param range - selection's range. If no active selection, range is null\n   * @deprecated use {@link MenuConfig} item onActivate property instead\n   */\n  surround?(range: Range | null): void;\n\n  /**\n   * Get SelectionUtils and detect if Tool was applied\n   * For example, after that Tool can highlight button or show some details\n   * @param {Selection} selection - current Selection\n   * @deprecated use {@link MenuConfig} item isActive property instead\n   */\n  checkState?(selection: Selection): boolean;\n\n  /**\n   * Make additional element with actions\n   * For example, input for the 'link' tool or textarea for the 'comment' tool\n   * @deprecated use {@link MenuConfig} item children to set item actions instead\n   */\n  renderActions?(): HTMLElement;\n\n  /**\n   * Function called with Inline Toolbar closing\n   * @deprecated 2020 10/02 - The new instance will be created each time the button is rendered. So clear is not needed.\n   *                          Better to create the 'destroy' method in a future.\n   */\n  clear?(): void;\n}\n\n\n/**\n * Describe constructor parameters\n */\nexport interface InlineToolConstructorOptions {\n  api: API;\n  config?: ToolConfig;\n}\n\nexport interface InlineToolConstructable extends BaseToolConstructable {\n  /**\n   * Constructor\n   *\n   * @param {InlineToolConstructorOptions} config - constructor parameters\n   */\n  new(config: InlineToolConstructorOptions): BaseTool;\n\n  /**\n   * Allows inline tool to be available in read-only mode\n   * Can be used, for example, by comments tool\n   */\n  isReadOnlySupported?: boolean;\n}\n"
  },
  {
    "path": "types/tools/menu-config.d.ts",
    "content": "import { PopoverItemDefaultBaseParams, PopoverItemHtmlParams, PopoverItemSeparatorParams, WithChildren } from '../utils/popover';\n\n/**\n * Menu configuration format.\n * Is used for defining Block Tunes Menu items via Block Tool's renderSettings(), Block Tune's render() and Inline Tool's render().\n */\nexport type MenuConfig = MenuConfigItem | MenuConfigItem[];\n\n/**\n * Common parameters for all kinds of default Menu Config items: with or without confirmation\n */\ntype MenuConfigDefaultBaseParams = PopoverItemDefaultBaseParams & {\n  /**\n   * Displayed text.\n   * Alias for title property\n   * \n   * @deprecated - use title property instead\n   */\n  label?: string\n};\n\n/**\n * Menu Config item with confirmation\n */\ntype MenuConfigItemDefaultWithConfirmationParams = Omit<MenuConfigDefaultBaseParams, 'onActivate'> & {\n  /**\n   * Items with confirmation should not have onActivate handler\n   */\n  onActivate?: never;\n\n  /**\n   * Menu Config item parameters that should be applied on item activation.\n   * May be used to ask user for confirmation before executing item activation handler.\n   */\n  confirmation: MenuConfigDefaultBaseParams;\n\n}\n\n/**\n * Default, non-separator and non-html Menu Config items type\n */\ntype MenuConfigItemDefaultParams = \n  MenuConfigItemDefaultWithConfirmationParams |\n  MenuConfigDefaultBaseParams |\n  WithChildren<MenuConfigDefaultBaseParams>;\n\n/**\n * Single Menu Config item\n */\ntype MenuConfigItem = \n  MenuConfigItemDefaultParams |\n  PopoverItemSeparatorParams |\n  PopoverItemHtmlParams |\n  WithChildren<PopoverItemHtmlParams>;\n"
  },
  {
    "path": "types/tools/paste-events.d.ts",
    "content": "/**\n * Event detail for tag substitution on paste\n */\nexport interface HTMLPasteEventDetail {\n  /**\n   * Pasted element\n   */\n  data: HTMLElement;\n}\n\n/**\n * Paste event for tag substitution\n */\nexport interface HTMLPasteEvent extends CustomEvent {\n  readonly detail: HTMLPasteEventDetail;\n}\n\n/**\n * Event detail for file substitution on paste\n */\nexport interface FilePasteEventDetail {\n  /**\n   * Pasted file\n   */\n  file: File;\n}\n\nexport interface FilePasteEvent extends CustomEvent {\n  readonly detail: FilePasteEventDetail;\n}\n\n/**\n * Event detail for pattern substitution on paste\n */\nexport interface PatternPasteEventDetail {\n  /**\n   * Pattern key\n   */\n  key: string;\n\n  /**\n   * Pasted string\n   */\n  data: string;\n}\n\nexport interface PatternPasteEvent extends CustomEvent {\n  readonly detail: PatternPasteEventDetail;\n}\n\nexport type PasteEvent = HTMLPasteEvent | FilePasteEvent | PatternPasteEvent;\nexport type PasteEventDetail = HTMLPasteEventDetail | FilePasteEventDetail | PatternPasteEventDetail;\n"
  },
  {
    "path": "types/tools/tool-config.d.ts",
    "content": "/**\n * Tool configuration object. Specified by Tool developer, so leave it as object\n */\nexport type ToolConfig<T extends object = any> = T;\n"
  },
  {
    "path": "types/tools/tool-settings.d.ts",
    "content": "import { ToolConfig } from './tool-config';\nimport { ToolConstructable, BlockToolData, MenuConfig, MenuConfigItem } from './index';\n\n/**\n * Tool may specify its toolbox configuration\n * It may include several entries as well\n */\nexport type ToolboxConfig = ToolboxConfigEntry | ToolboxConfigEntry[];\n\n/**\n * Tool's Toolbox settings\n */\nexport interface ToolboxConfigEntry {\n  /**\n   * Tool title for Toolbox\n   */\n  title?: string;\n\n  /**\n   * HTML string with an icon for Toolbox\n   */\n  icon?: string;\n\n  /**\n   * May contain overrides for tool default data\n   */\n  data?: BlockToolData\n}\n\n/**\n * Object passed to the Tool's constructor by {@link EditorConfig#tools}\n *\n * @template Config - the structure describing a config object supported by the tool\n */\nexport interface ExternalToolSettings<Config extends object = any> {\n\n  /**\n   * Tool's class\n   */\n  class: ToolConstructable;\n\n  /**\n   * User configuration object that will be passed to the Tool's constructor\n   */\n  config?: ToolConfig<Config>;\n\n  /**\n   * Is need to show Inline Toolbar.\n   * Can accept array of Tools for InlineToolbar or boolean.\n   */\n  inlineToolbar?: boolean | string[];\n\n  /**\n   * BlockTunes for Tool\n   * Can accept array of tune names or boolean.\n   */\n  tunes?: boolean | string[];\n\n  /**\n   * Define shortcut that will render Tool\n   */\n  shortcut?: string;\n\n  /**\n   * Tool's Toolbox settings\n   * It will be hidden from Toolbox when false is specified.\n   */\n  toolbox?: ToolboxConfig | false;\n}\n\n/**\n * Tool's tunes configuration.\n * @deprecated use {@link MenuConfig} type instead\n */\nexport type TunesMenuConfig = MenuConfig;\n\n/**\n * Single Tunes Menu Config item\n * @deprecated use {@link MenuConfigItem} type instead\n */\nexport type TunesMenuConfigItem = MenuConfigItem;\n\n/**\n * For internal Tools 'class' property is optional\n */\nexport type InternalToolSettings<Config extends object = any> = Omit<ExternalToolSettings<Config>, 'class'> & Partial<Pick<ExternalToolSettings<Config>, 'class'>>;\n\n/**\n * Union of external and internal Tools settings\n */\nexport type ToolSettings<Config extends object = any> = InternalToolSettings<Config> | ExternalToolSettings<Config>;\n"
  },
  {
    "path": "types/tools/tool.d.ts",
    "content": "import {API} from '../index';\nimport {ToolConfig} from './tool-config';\nimport {SanitizerConfig} from '../configs';\nimport {MenuConfig} from './menu-config';\n\n/**\n * Abstract interface of all Tools\n */\nexport interface BaseTool<RenderReturnType = HTMLElement> {\n  /**\n   * Tool`s render method\n   *\n   * For Inline Tools may return either HTMLElement (deprecated) or {@link MenuConfig}\n   * @see https://editorjs.io/menu-config\n   *\n   * For Block Tools returns tool`s wrapper html element\n   */\n  render(): RenderReturnType | Promise<RenderReturnType>;\n}\n\nexport interface BaseToolConstructorOptions<C extends object = any> {\n  /**\n   * Editor.js API\n   */\n  api: API;\n\n  /**\n   * Tool configuration\n   */\n  config?: ToolConfig<C>;\n}\n\nexport interface BaseToolConstructable {\n  /**\n   * Define Tool type as Inline\n   */\n  isInline?: boolean;\n\n  /**\n   * Tool`s sanitizer configuration\n   */\n  sanitize?: SanitizerConfig;\n\n  /**\n   * Title of Inline Tool.\n   * @deprecated use {@link MenuConfig} item title instead\n   */\n  title?: string;\n\n  /**\n   * Tool`s prepare method. Can be async\n   * @param data\n   */\n  prepare?(data: {toolName: string, config: ToolConfig}): void | Promise<void>;\n\n  /**\n   * Tool`s reset method to clean up anything set by prepare. Can be async\n   */\n  reset?(): void | Promise<void>;\n}\n"
  },
  {
    "path": "types/utils/popover/hint.d.ts",
    "content": "/**\n * Hint parameters\n */\nexport interface HintParams {\n  /**\n   * Title of the hint\n   */\n  title: string;\n\n  /**\n   * Secondary text to be displayed below the title\n   */\n  description?: string;\n\n  /**\n   * Horizontal alignment of the hint content. Default is 'start'\n   */\n  alignment?: HintTextAlignment;\n}\n\n/**\n * Possible hint positions\n */\nexport type HintPosition = 'top' | 'bottom' | 'left' | 'right';\n\n/**\n * Horizontal alignment of the hint content\n */\nexport type HintTextAlignment = 'start' | 'center';\n"
  },
  {
    "path": "types/utils/popover/index.d.ts",
    "content": "export type * from './hint';\nexport type * from './popover-item';\nexport * from './popover-item-type';\nexport type * from './popover';\nexport * from './popover-event';\n"
  },
  {
    "path": "types/utils/popover/popover-event.ts",
    "content": "\n/**\n * Event that can be triggered by the Popover\n */\nexport enum PopoverEvent {\n  /**\n   * When popover closes\n   */\n  Closed = 'closed',\n\n  /**\n   * When it closes because item with 'closeOnActivate' property set was clicked\n   */\n  ClosedOnActivate = 'closed-on-activate',\n}\n"
  },
  {
    "path": "types/utils/popover/popover-item-type.ts",
    "content": "/**\n * Popover item types\n */\nexport enum PopoverItemType {\n  /** Regular item with icon, title and other properties */\n  Default = 'default',\n\n  /** Gray line used to separate items from each other */\n  Separator = 'separator',\n\n  /** Item with custom html content */\n  Html = 'html'\n}\n"
  },
  {
    "path": "types/utils/popover/popover-item.d.ts",
    "content": "import { HintParams, HintPosition, HintTextAlignment } from \"./hint\";\nimport { PopoverItemType } from \"./popover-item-type\";\n\nexport { PopoverItemType } from './popover-item-type';\n\n/**\n * Represents popover item children configuration\n */\nexport interface PopoverItemChildren {\n  /**\n   * True if children items should be searchable\n   */\n  searchable?: boolean;\n\n  /**\n   * True if popover with children should be displayed instantly and not after item click/hover.\n   * False by default.\n   * Now is used only in the inline popover.\n   */\n  isOpen?: boolean;\n\n  /**\n   * False if keyboard navigation should be disabled in the children popover.\n   * True by default\n   */\n  isFlippable?: boolean;\n\n /**\n  * Items of nested popover that should be open on the current item hover/click (depending on platform)\n  */\n  items?: PopoverItemParams[];\n\n  /**\n   * Called once children popover is opened\n   */\n  onOpen?: () => void;\n\n  /**\n   * Called once children popover is closed\n   */\n  onClose?: () => void;\n}\n\n/**\n * Adds children property to the item\n */\nexport type WithChildren<T> = Omit<T, 'onActivate'> & {\n  /**\n   * Popover item children configuration\n   */\n  children: PopoverItemChildren;\n\n  /**\n   * Items with children should not have onActivate handler\n   */\n  onActivate?: never;\n};\n\n/**\n * Represents popover item with confirmation.\n */\nexport type PopoverItemDefaultWithConfirmationParams = Omit<PopoverItemDefaultBaseParams, 'onActivate'> & {\n  /**\n   * Popover item parameters that should be applied on item activation.\n   * May be used to ask user for confirmation before executing popover item activation handler.\n   */\n  confirmation: PopoverItemDefaultBaseParams;\n\n  /**\n   * Items with confirmation should not have onActivate handler\n   */\n  onActivate?: never;\n};\n\n/**\n * Represents popover item separator.\n * Special item type that is used to separate items in the popover.\n */\nexport interface PopoverItemSeparatorParams {\n  /**\n   * Item type\n   */\n  type: PopoverItemType.Separator;\n}\n\n/**\n * Represents popover item with custom html content\n */\nexport interface PopoverItemHtmlParams {\n  /**\n   * Item type\n   */\n  type: PopoverItemType.Html;\n\n  /**\n   * Custom html content to be displayed in the popover\n   */\n  element: HTMLElement;\n\n  /**\n   * Hint data to be displayed on item hover\n   */\n  hint?: HintParams;\n\n  /**\n   * True if popover should close once item is activated\n   */\n  closeOnActivate?: boolean;\n\n  /**\n   * Item name\n   * Used in data attributes needed for cypress tests\n   */\n  name?: string;\n}\n\n/**\n * Common parameters for all kinds of default popover items: with or without confirmation\n */\nexport interface PopoverItemDefaultBaseParams {\n  /**\n   * Item type\n   */\n  type?: PopoverItemType.Default;\n\n  /**\n   * Displayed text\n   */\n  title?: string;\n\n  /**\n   * Item icon to be appeared near a title\n   */\n  icon?: string;\n\n  /**\n   * Additional displayed text\n   */\n  secondaryLabel?: string;\n\n  /**\n   * True if item should be highlighted as active\n   */\n  isActive?: boolean | (() => boolean);\n\n  /**\n   * True if item should be disabled\n   */\n  isDisabled?: boolean;\n\n  /**\n   * True if popover should close once item is activated\n   */\n  closeOnActivate?: boolean;\n\n  /**\n   * Item name\n   * Used in data attributes needed for shortcuts work and for cypress tests\n   */\n  name?: string;\n\n  /**\n   * Defines whether item should toggle on click.\n   * Can be represented as boolean value or a string key.\n   * In case of string, works like radio buttons group and highlights as inactive any other item that has same toggle key value.\n   */\n  toggle?: boolean | string;\n\n  /**\n   * Hint data to be displayed on item hover\n   */\n  hint?: HintParams;\n\n  /**\n   * Popover item activation handler\n   *\n   * @param item - activated item\n   * @param event - event that initiated item activation\n   */\n  onActivate: (item: PopoverItemParams, event?: PointerEvent) => void;\n}\n\n/**\n * Default, non-separator and non-html popover items type\n */\nexport type PopoverItemDefaultParams =\n  PopoverItemDefaultBaseParams |\n  PopoverItemDefaultWithConfirmationParams |\n  WithChildren<PopoverItemDefaultBaseParams>;\n\n/**\n * Represents single popover item\n */\nexport type PopoverItemParams =\n  PopoverItemDefaultParams |\n  PopoverItemSeparatorParams |\n  PopoverItemHtmlParams |\n  WithChildren<PopoverItemHtmlParams>;\n\n/**\n * Parameters of how to render hint for the popover item\n */\ntype PopoverItemHintRenderParams = {\n  /**\n   * Hint position relative to the item\n   */\n  position?: HintPosition;\n\n  /**\n   * Horizontal alignment of the hint content.\n   * 'start' by default.\n   */\n  alignment?: HintTextAlignment;\n\n  /**\n   * If false, hint will not be rendered.\n   * True by default.\n   * Used to disable hints on mobile popover\n   */\n  enabled?: boolean;\n};\n\n\n/**\n * Popover item render params.\n * The parameters that are not set by user via popover api but rather depend on technical implementation\n */\nexport type PopoverItemRenderParamsMap = {\n  [PopoverItemType.Default]?: {\n    /**\n     * Wrapper tag for the item.\n     * Div by default\n     */\n    wrapperTag?: 'div' | 'button';\n\n    /**\n     * Hint render params\n     */\n    hint?: PopoverItemHintRenderParams\n  };\n\n  [PopoverItemType.Html]?: {\n    /**\n     * Hint render params\n     */\n    hint?: PopoverItemHintRenderParams\n  };\n};\n"
  },
  {
    "path": "types/utils/popover/popover.d.ts",
    "content": "import { PopoverItemParams } from './popover-item';\nimport { PopoverEvent } from './popover-event';\n\n/**\n * Params required to render popover\n */\nexport interface PopoverParams {\n  /**\n   * Popover items config\n   */\n  items: PopoverItemParams[];\n\n  /**\n   * Element of the page that creates 'scope' of the popover.\n   * Depending on its size popover position will be calculated\n   */\n  scopeElement?: HTMLElement;\n\n  /**\n   * True if popover should contain search field\n   */\n  searchable?: boolean;\n\n  /**\n   * False if keyboard navigation should be disabled.\n   * True by default\n   */\n  flippable?: boolean;\n\n  /**\n   * Popover texts overrides\n   */\n  messages?: PopoverMessages\n\n  /**\n   * CSS class name for popover root element\n   */\n  class?: string;\n\n  /**\n   * Popover nesting level. 0 value means that it is a root popover\n   */\n  nestingLevel?: number;\n}\n\n\n/**\n * Texts used inside popover\n */\nexport interface PopoverMessages {\n  /** Text displayed when search has no results */\n  nothingFound?: string;\n\n  /** Search input label */\n  search?: string\n}\n\n\n/**\n * Events fired by the Popover\n */\nexport interface PopoverEventMap {\n  /**\n   * Fired when popover closes\n   */\n  [PopoverEvent.Closed]: undefined;\n\n  /**\n   * Fired when popover closes because item with 'closeOnActivate' property set was clicked\n   * Value is the item that was clicked\n   */\n  [PopoverEvent.ClosedOnActivate]: undefined;\n}\n\n/**\n * HTML elements required to display popover\n */\nexport interface PopoverNodes {\n  /** Root popover element */\n  popover: HTMLElement;\n\n  /** Wraps all the visible popover elements, has background and rounded corners */\n  popoverContainer: HTMLElement;\n\n  /** Message displayed when no items found while searching */\n  nothingFoundMessage: HTMLElement;\n\n  /** Popover items wrapper */\n  items: HTMLElement;\n}\n\n/**\n * HTML elements required to display mobile popover\n */\nexport interface PopoverMobileNodes extends PopoverNodes {\n  /** Popover header element */\n  header: HTMLElement;\n\n  /** Overlay, displayed under popover on mobile */\n  overlay: HTMLElement;\n}\n"
  },
  {
    "path": "vite.config.js",
    "content": "import path from 'path';\n\nimport cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js';\nimport license from 'rollup-plugin-license';\n\nimport * as pkg from './package.json';\n\nconst NODE_ENV = process.argv.mode || 'development';\nconst VERSION = pkg.version;\n\n/**\n * Trick to use Vite server.open option on macOS\n * @see https://github.com/facebook/create-react-app/pull/1690#issuecomment-283518768\n */\nprocess.env.BROWSER = 'open';\n\nexport default {\n  build: {\n    copyPublicDir: false,\n    lib: {\n      entry: path.resolve(__dirname, 'src', 'codex.ts'),\n      name: 'EditorJS',\n      fileName: 'editorjs',\n    },\n    rollupOptions: {\n      plugins: [\n        license({\n          thirdParty: {\n            allow: {\n              test: (dependency) => {\n                // Manually allow html-janitor (https://github.com/guardian/html-janitor/blob/master/LICENSE)\n                // because of missing LICENSE file in published package\n                if (dependency.name === 'html-janitor') {\n                  return true;\n                }\n\n                // Return false for unlicensed dependencies.\n                if (!dependency.license) {\n                  return false;\n                }\n\n                // Allow MIT and Apache-2.0 licenses.\n                return ['MIT', 'Apache-2.0'].includes(dependency.license);\n              },\n              failOnUnlicensed: true,\n              failOnViolation: true,\n            },\n            output: path.resolve(__dirname, 'dist', 'vendor.LICENSE.txt'),\n          },\n        }),\n      ],\n    },\n  },\n\n  define: {\n    'NODE_ENV': JSON.stringify(NODE_ENV),\n    'VERSION': JSON.stringify(VERSION),\n  },\n\n  resolve: {\n    alias: {\n      '@/types': path.resolve(__dirname, './types'),\n    },\n  },\n\n  server: {\n    port: 3303,\n    open: true,\n  },\n\n  plugins: [\n    cssInjectedByJsPlugin(),\n  ],\n};\n"
  },
  {
    "path": "vite.config.test.js",
    "content": "import path from 'path';\nimport { defineConfig } from 'vite';\n\nexport default defineConfig({\n  build: {\n    minify: false,\n    sourcemap: true,\n  },\n  define: {\n    'NODE_ENV': JSON.stringify('test'),\n    'VERSION': JSON.stringify('test-version'),\n  },\n  resolve: {\n    alias: {\n      '@/types': path.resolve(__dirname, './types'),\n    },\n  },\n});\n"
  }
]