From 8471bc299d7fda83e9e0a0024724b1a19d1aea7e Mon Sep 17 00:00:00 2001 From: qinluhe <108015703+qinluhe@users.noreply.github.com> Date: Wed, 22 Mar 2023 10:46:01 +0800 Subject: [PATCH] feat: block list virtualized scroll (#2023) * feat: block list virtualized scroll * feat: block selection * refactor: block editor * fix: block selection scroll * fix: ts error --- frontend/appflowy_tauri/package.json | 3 + frontend/appflowy_tauri/pnpm-lock.yaml | 735 +++++++++++------- .../src/appflowy_app/block_editor/block.ts | 28 - .../block_editor/blocks/text_block/index.ts | 71 ++ .../blocks/text_block/text_selection.ts | 35 + .../appflowy_app/block_editor/core/block.ts | 107 +++ .../block_editor/core/block_chain.ts | 225 ++++++ .../block_editor/core/op_adapter.ts | 16 + .../block_editor/core/operation.ts | 153 ++++ .../appflowy_app/block_editor/core/sync.ts | 48 ++ .../src/appflowy_app/block_editor/index.ts | 70 +- .../src/appflowy_app/block_editor/rect.ts | 66 -- .../src/appflowy_app/block_editor/tree.ts | 140 ---- .../block_editor/view/block_position.ts | 73 ++ .../block_editor/view/region_grid.ts | 81 ++ .../appflowy_app/block_editor/view/tree.ts | 165 ++++ .../block_editor/view/tree_node.ts | 59 ++ .../HoveringToolbar/FormatButton.tsx | 29 +- .../components/HoveringToolbar/FormatIcon.tsx | 20 + .../components/HoveringToolbar/components.tsx | 5 - .../components/HoveringToolbar/index.hooks.ts | 36 + .../components/HoveringToolbar/index.tsx | 29 +- .../BlockComponent/BlockComponet.hooks.ts | 36 + .../components/block/BlockComponent/index.tsx | 91 +++ .../block/BlockList/BlockComponent.tsx | 39 - .../block/BlockList/BlockList.hooks.tsx | 92 +++ .../block/BlockList/BlockListTitle.tsx | 18 + .../block/BlockList/ListFallbackComponent.tsx | 31 + .../components/block/BlockList/index.tsx | 85 +- .../BlockSelection/BlockSelection.hooks.tsx | 137 ++++ .../components/block/BlockSelection/index.tsx | 18 + .../components/block/CodeBlock/index.tsx | 6 +- .../components/block/ColumnBlock/index.tsx | 15 +- .../components/block/HeadingBlock/index.tsx | 14 +- .../block/ListBlock/BulletedListBlock.tsx | 8 +- .../block/ListBlock/ColumnListBlock.tsx | 6 +- .../block/ListBlock/NumberedListBlock.tsx | 23 +- .../components/block/ListBlock/index.tsx | 14 +- .../components/block/PageBlock/index.tsx | 6 +- .../components/block/TextBlock/index.hooks.ts | 98 +++ .../components/block/TextBlock/index.tsx | 90 +-- .../src/appflowy_app/constants/toolbar.ts | 14 + .../src/appflowy_app/interfaces/index.ts | 68 +- .../src/appflowy_app/utils/block.ts | 25 + .../src/appflowy_app/utils/block_context.ts | 8 - .../src/appflowy_app/utils/block_selection.ts | 36 + .../src/appflowy_app/utils/editor/format.ts | 25 - .../src/appflowy_app/utils/editor/hotkey.ts | 22 - .../src/appflowy_app/utils/editor/toolbar.ts | 28 - .../src/appflowy_app/utils/slate/context.ts | 6 + .../src/appflowy_app/utils/slate/toolbar.ts | 16 +- .../src/appflowy_app/utils/tool.ts | 26 + .../appflowy_app/views/DocumentPage.hooks.ts | 597 +++++++++++++- .../src/appflowy_app/views/DocumentPage.tsx | 23 +- 54 files changed, 2973 insertions(+), 942 deletions(-) delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/block.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/index.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/text_selection.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block_chain.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/core/op_adapter.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/core/operation.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/core/sync.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/rect.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/tree.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/view/block_position.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/view/region_grid.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree_node.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/FormatIcon.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/components.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.hooks.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/BlockComponet.hooks.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/index.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockComponent.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockList.hooks.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockListTitle.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/ListFallbackComponent.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/BlockSelection.hooks.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/index.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.hooks.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/utils/block.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/utils/block_context.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/utils/block_selection.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/utils/editor/format.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/utils/editor/hotkey.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/utils/editor/toolbar.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/utils/slate/context.ts diff --git a/frontend/appflowy_tauri/package.json b/frontend/appflowy_tauri/package.json index e0fa62458c..8215381041 100644 --- a/frontend/appflowy_tauri/package.json +++ b/frontend/appflowy_tauri/package.json @@ -20,7 +20,9 @@ "@mui/icons-material": "^5.11.11", "@mui/material": "^5.11.12", "@reduxjs/toolkit": "^1.9.2", + "@tanstack/react-virtual": "3.0.0-beta.54", "@tauri-apps/api": "^1.2.0", + "events": "^3.3.0", "google-protobuf": "^3.21.2", "i18next": "^22.4.10", "i18next-browser-languagedetector": "^7.0.1", @@ -40,6 +42,7 @@ "slate": "^0.91.4", "slate-react": "^0.91.9", "ts-results": "^3.3.0", + "ulid": "^2.3.0", "utf8": "^3.0.0" }, "devDependencies": { diff --git a/frontend/appflowy_tauri/pnpm-lock.yaml b/frontend/appflowy_tauri/pnpm-lock.yaml index a6c00ac119..4402ceca71 100644 --- a/frontend/appflowy_tauri/pnpm-lock.yaml +++ b/frontend/appflowy_tauri/pnpm-lock.yaml @@ -6,12 +6,11 @@ specifiers: '@mui/icons-material': ^5.11.11 '@mui/material': ^5.11.12 '@reduxjs/toolkit': ^1.9.2 + '@tanstack/react-virtual': 3.0.0-beta.54 '@tauri-apps/api': ^1.2.0 '@tauri-apps/cli': ^1.2.2 '@types/google-protobuf': ^3.15.6 '@types/is-hotkey': ^0.1.7 - '@types/jest': ^29.4.0 - '@types/mocha': ^10.0.1 '@types/node': ^18.7.10 '@types/react': ^18.0.15 '@types/react-dom': ^18.0.6 @@ -22,11 +21,12 @@ specifiers: autoprefixer: ^10.4.13 eslint: ^8.34.0 eslint-plugin-react: ^7.32.2 + events: ^3.3.0 google-protobuf: ^3.21.2 i18next: ^22.4.10 i18next-browser-languagedetector: ^7.0.1 is-hotkey: ^0.2.0 - jest: ^29.4.3 + jest: ^29.5.0 nanoid: ^4.0.0 postcss: ^8.4.21 prettier: 2.8.4 @@ -43,9 +43,9 @@ specifiers: slate: ^0.91.4 slate-react: ^0.91.9 tailwindcss: ^3.2.7 - ts-jest: ^29.0.5 ts-results: ^3.3.0 typescript: ^4.6.4 + ulid: ^2.3.0 utf8: ^3.0.0 vite: ^4.0.0 @@ -55,12 +55,14 @@ dependencies: '@mui/icons-material': 5.11.11_ao76n7r2cajsoyr3cbwrn7geoi '@mui/material': 5.11.12_xqeqsl5kvjjtyxwyi3jhw3yuli '@reduxjs/toolkit': 1.9.3_k4ae6lp43ej6mezo3ztvx6pykq + '@tanstack/react-virtual': 3.0.0-beta.54_react@18.2.0 '@tauri-apps/api': 1.2.0 + events: 3.3.0 google-protobuf: 3.21.2 i18next: 22.4.10 i18next-browser-languagedetector: 7.0.1 is-hotkey: 0.2.0 - jest: 29.4.3_@types+node@18.14.6 + jest: 29.5.0_@types+node@18.14.6 nanoid: 4.0.1 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 @@ -74,14 +76,13 @@ dependencies: slate: 0.91.4 slate-react: 0.91.9_6tgy34rvmll7duwkm4ydcekf3u ts-results: 3.3.0 + ulid: 2.3.0 utf8: 3.0.0 devDependencies: '@tauri-apps/cli': 1.2.3 '@types/google-protobuf': 3.15.6 '@types/is-hotkey': 0.1.7 - '@types/jest': 29.4.0 - '@types/mocha': 10.0.1 '@types/node': 18.14.6 '@types/react': 18.0.28 '@types/react-dom': 18.0.11 @@ -96,7 +97,6 @@ devDependencies: prettier: 2.8.4 prettier-plugin-tailwindcss: 0.2.4_prettier@2.8.4 tailwindcss: 3.2.7_postcss@8.4.21 - ts-jest: 29.0.5_orzzknleilowtsz34rkaotjvzm typescript: 4.9.5 vite: 4.1.4_@types+node@18.14.6 @@ -261,6 +261,7 @@ packages: dependencies: '@babel/core': 7.21.0 '@babel/helper-plugin-utils': 7.20.2 + dev: false /@babel/plugin-syntax-bigint/7.8.3_@babel+core@7.21.0: resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} @@ -269,6 +270,7 @@ packages: dependencies: '@babel/core': 7.21.0 '@babel/helper-plugin-utils': 7.20.2 + dev: false /@babel/plugin-syntax-class-properties/7.12.13_@babel+core@7.21.0: resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} @@ -277,6 +279,7 @@ packages: dependencies: '@babel/core': 7.21.0 '@babel/helper-plugin-utils': 7.20.2 + dev: false /@babel/plugin-syntax-import-meta/7.10.4_@babel+core@7.21.0: resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} @@ -285,6 +288,7 @@ packages: dependencies: '@babel/core': 7.21.0 '@babel/helper-plugin-utils': 7.20.2 + dev: false /@babel/plugin-syntax-json-strings/7.8.3_@babel+core@7.21.0: resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} @@ -293,6 +297,7 @@ packages: dependencies: '@babel/core': 7.21.0 '@babel/helper-plugin-utils': 7.20.2 + dev: false /@babel/plugin-syntax-jsx/7.18.6_@babel+core@7.21.0: resolution: {integrity: sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==} @@ -302,6 +307,7 @@ packages: dependencies: '@babel/core': 7.21.0 '@babel/helper-plugin-utils': 7.20.2 + dev: false /@babel/plugin-syntax-logical-assignment-operators/7.10.4_@babel+core@7.21.0: resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} @@ -310,6 +316,7 @@ packages: dependencies: '@babel/core': 7.21.0 '@babel/helper-plugin-utils': 7.20.2 + dev: false /@babel/plugin-syntax-nullish-coalescing-operator/7.8.3_@babel+core@7.21.0: resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} @@ -318,6 +325,7 @@ packages: dependencies: '@babel/core': 7.21.0 '@babel/helper-plugin-utils': 7.20.2 + dev: false /@babel/plugin-syntax-numeric-separator/7.10.4_@babel+core@7.21.0: resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} @@ -326,6 +334,7 @@ packages: dependencies: '@babel/core': 7.21.0 '@babel/helper-plugin-utils': 7.20.2 + dev: false /@babel/plugin-syntax-object-rest-spread/7.8.3_@babel+core@7.21.0: resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} @@ -334,6 +343,7 @@ packages: dependencies: '@babel/core': 7.21.0 '@babel/helper-plugin-utils': 7.20.2 + dev: false /@babel/plugin-syntax-optional-catch-binding/7.8.3_@babel+core@7.21.0: resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} @@ -342,6 +352,7 @@ packages: dependencies: '@babel/core': 7.21.0 '@babel/helper-plugin-utils': 7.20.2 + dev: false /@babel/plugin-syntax-optional-chaining/7.8.3_@babel+core@7.21.0: resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} @@ -350,6 +361,7 @@ packages: dependencies: '@babel/core': 7.21.0 '@babel/helper-plugin-utils': 7.20.2 + dev: false /@babel/plugin-syntax-top-level-await/7.14.5_@babel+core@7.21.0: resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} @@ -359,6 +371,7 @@ packages: dependencies: '@babel/core': 7.21.0 '@babel/helper-plugin-utils': 7.20.2 + dev: false /@babel/plugin-syntax-typescript/7.20.0_@babel+core@7.21.0: resolution: {integrity: sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==} @@ -368,6 +381,7 @@ packages: dependencies: '@babel/core': 7.21.0 '@babel/helper-plugin-utils': 7.20.2 + dev: false /@babel/plugin-transform-react-jsx-self/7.21.0_@babel+core@7.21.0: resolution: {integrity: sha512-f/Eq+79JEu+KUANFks9UZCcvydOOGMgF7jBrcwjHa5jTZD8JivnhCJYvmlhR/WTXBWonDExPoW0eO/CR4QJirA==} @@ -431,6 +445,7 @@ packages: /@bcoe/v8-coverage/0.2.3: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + dev: false /@emotion/babel-plugin/11.10.6: resolution: {integrity: sha512-p2dAqtVrkhSa7xz1u/m9eHYdLi+en8NowrmXeF/dKtJpU8lCWli8RUAati7NcSl0afsBott48pdnANuD0wh9QQ==} @@ -797,24 +812,27 @@ packages: get-package-type: 0.1.0 js-yaml: 3.14.1 resolve-from: 5.0.0 + dev: false /@istanbuljs/schema/0.1.3: resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} + dev: false - /@jest/console/29.4.3: - resolution: {integrity: sha512-W/o/34+wQuXlgqlPYTansOSiBnuxrTv61dEVkA6HNmpcgHLUjfaUbdqt6oVvOzaawwo9IdW9QOtMgQ1ScSZC4A==} + /@jest/console/29.5.0: + resolution: {integrity: sha512-NEpkObxPwyw/XxZVLPmAGKE89IQRp4puc6IQRPru6JKd1M3fW9v1xM1AnzIJE65hbCkzQAdnL8P47e9hzhiYLQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.4.3 + '@jest/types': 29.5.0 '@types/node': 18.14.6 chalk: 4.1.2 - jest-message-util: 29.4.3 - jest-util: 29.4.3 + jest-message-util: 29.5.0 + jest-util: 29.5.0 slash: 3.0.0 + dev: false - /@jest/core/29.4.3: - resolution: {integrity: sha512-56QvBq60fS4SPZCuM7T+7scNrkGIe7Mr6PVIXUpu48ouvRaWOFqRPV91eifvFM0ay2HmfswXiGf97NGUN5KofQ==} + /@jest/core/29.5.0: + resolution: {integrity: sha512-28UzQc7ulUrOQw1IsN/kv1QES3q2kkbl/wGslyhAclqZ/8cMdB5M68BffkIdSJgKBUt50d3hbwJ92XESlE7LiQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 @@ -822,86 +840,92 @@ packages: node-notifier: optional: true dependencies: - '@jest/console': 29.4.3 - '@jest/reporters': 29.4.3 - '@jest/test-result': 29.4.3 - '@jest/transform': 29.4.3 - '@jest/types': 29.4.3 + '@jest/console': 29.5.0 + '@jest/reporters': 29.5.0 + '@jest/test-result': 29.5.0 + '@jest/transform': 29.5.0 + '@jest/types': 29.5.0 '@types/node': 18.14.6 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.8.0 exit: 0.1.2 graceful-fs: 4.2.10 - jest-changed-files: 29.4.3 - jest-config: 29.4.3_@types+node@18.14.6 - jest-haste-map: 29.4.3 - jest-message-util: 29.4.3 + jest-changed-files: 29.5.0 + jest-config: 29.5.0_@types+node@18.14.6 + jest-haste-map: 29.5.0 + jest-message-util: 29.5.0 jest-regex-util: 29.4.3 - jest-resolve: 29.4.3 - jest-resolve-dependencies: 29.4.3 - jest-runner: 29.4.3 - jest-runtime: 29.4.3 - jest-snapshot: 29.4.3 - jest-util: 29.4.3 - jest-validate: 29.4.3 - jest-watcher: 29.4.3 + jest-resolve: 29.5.0 + jest-resolve-dependencies: 29.5.0 + jest-runner: 29.5.0 + jest-runtime: 29.5.0 + jest-snapshot: 29.5.0 + jest-util: 29.5.0 + jest-validate: 29.5.0 + jest-watcher: 29.5.0 micromatch: 4.0.5 - pretty-format: 29.4.3 + pretty-format: 29.5.0 slash: 3.0.0 strip-ansi: 6.0.1 transitivePeerDependencies: - supports-color - ts-node + dev: false - /@jest/environment/29.4.3: - resolution: {integrity: sha512-dq5S6408IxIa+lr54zeqce+QgI+CJT4nmmA+1yzFgtcsGK8c/EyiUb9XQOgz3BMKrRDfKseeOaxj2eO8LlD3lA==} + /@jest/environment/29.5.0: + resolution: {integrity: sha512-5FXw2+wD29YU1d4I2htpRX7jYnAyTRjP2CsXQdo9SAM8g3ifxWPSV0HnClSn71xwctr0U3oZIIH+dtbfmnbXVQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/fake-timers': 29.4.3 - '@jest/types': 29.4.3 + '@jest/fake-timers': 29.5.0 + '@jest/types': 29.5.0 '@types/node': 18.14.6 - jest-mock: 29.4.3 + jest-mock: 29.5.0 + dev: false - /@jest/expect-utils/29.4.3: - resolution: {integrity: sha512-/6JWbkxHOP8EoS8jeeTd9dTfc9Uawi+43oLKHfp6zzux3U2hqOOVnV3ai4RpDYHOccL6g+5nrxpoc8DmJxtXVQ==} + /@jest/expect-utils/29.5.0: + resolution: {integrity: sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: jest-get-type: 29.4.3 + dev: false - /@jest/expect/29.4.3: - resolution: {integrity: sha512-iktRU/YsxEtumI9zsPctYUk7ptpC+AVLLk1Ax3AsA4g1C+8OOnKDkIQBDHtD5hA/+VtgMd5AWI5gNlcAlt2vxQ==} + /@jest/expect/29.5.0: + resolution: {integrity: sha512-PueDR2HGihN3ciUNGr4uelropW7rqUfTiOn+8u0leg/42UhblPxHkfoh0Ruu3I9Y1962P3u2DY4+h7GVTSVU6g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - expect: 29.4.3 - jest-snapshot: 29.4.3 + expect: 29.5.0 + jest-snapshot: 29.5.0 transitivePeerDependencies: - supports-color + dev: false - /@jest/fake-timers/29.4.3: - resolution: {integrity: sha512-4Hote2MGcCTWSD2gwl0dwbCpBRHhE6olYEuTj8FMowdg3oQWNKr2YuxenPQYZ7+PfqPY1k98wKDU4Z+Hvd4Tiw==} + /@jest/fake-timers/29.5.0: + resolution: {integrity: sha512-9ARvuAAQcBwDAqOnglWq2zwNIRUDtk/SCkp/ToGEhFv5r86K21l+VEs0qNTaXtyiY0lEePl3kylijSYJQqdbDg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.4.3 + '@jest/types': 29.5.0 '@sinonjs/fake-timers': 10.0.2 '@types/node': 18.14.6 - jest-message-util: 29.4.3 - jest-mock: 29.4.3 - jest-util: 29.4.3 + jest-message-util: 29.5.0 + jest-mock: 29.5.0 + jest-util: 29.5.0 + dev: false - /@jest/globals/29.4.3: - resolution: {integrity: sha512-8BQ/5EzfOLG7AaMcDh7yFCbfRLtsc+09E1RQmRBI4D6QQk4m6NSK/MXo+3bJrBN0yU8A2/VIcqhvsOLFmziioA==} + /@jest/globals/29.5.0: + resolution: {integrity: sha512-S02y0qMWGihdzNbUiqSAiKSpSozSuHX5UYc7QbnHP+D9Lyw8DgGGCinrN9uSuHPeKgSSzvPom2q1nAtBvUsvPQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 29.4.3 - '@jest/expect': 29.4.3 - '@jest/types': 29.4.3 - jest-mock: 29.4.3 + '@jest/environment': 29.5.0 + '@jest/expect': 29.5.0 + '@jest/types': 29.5.0 + jest-mock: 29.5.0 transitivePeerDependencies: - supports-color + dev: false - /@jest/reporters/29.4.3: - resolution: {integrity: sha512-sr2I7BmOjJhyqj9ANC6CTLsL4emMoka7HkQpcoMRlhCbQJjz2zsRzw0BDPiPyEFDXAbxKgGFYuQZiSJ1Y6YoTg==} + /@jest/reporters/29.5.0: + resolution: {integrity: sha512-D05STXqj/M8bP9hQNSICtPqz97u7ffGzZu+9XLucXhkOFBqKcXe04JLZOgIekOxdb73MAoBUFnqvf7MCpKk5OA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 @@ -910,10 +934,10 @@ packages: optional: true dependencies: '@bcoe/v8-coverage': 0.2.3 - '@jest/console': 29.4.3 - '@jest/test-result': 29.4.3 - '@jest/transform': 29.4.3 - '@jest/types': 29.4.3 + '@jest/console': 29.5.0 + '@jest/test-result': 29.5.0 + '@jest/transform': 29.5.0 + '@jest/types': 29.5.0 '@jridgewell/trace-mapping': 0.3.17 '@types/node': 18.14.6 chalk: 4.1.2 @@ -926,21 +950,23 @@ packages: istanbul-lib-report: 3.0.0 istanbul-lib-source-maps: 4.0.1 istanbul-reports: 3.1.5 - jest-message-util: 29.4.3 - jest-util: 29.4.3 - jest-worker: 29.4.3 + jest-message-util: 29.5.0 + jest-util: 29.5.0 + jest-worker: 29.5.0 slash: 3.0.0 string-length: 4.0.2 strip-ansi: 6.0.1 v8-to-istanbul: 9.1.0 transitivePeerDependencies: - supports-color + dev: false /@jest/schemas/29.4.3: resolution: {integrity: sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@sinclair/typebox': 0.25.24 + dev: false /@jest/source-map/29.4.3: resolution: {integrity: sha512-qyt/mb6rLyd9j1jUts4EQncvS6Yy3PM9HghnNv86QBlV+zdL2inCdK1tuVlL+J+lpiw2BI67qXOrX3UurBqQ1w==} @@ -949,49 +975,53 @@ packages: '@jridgewell/trace-mapping': 0.3.17 callsites: 3.1.0 graceful-fs: 4.2.10 + dev: false - /@jest/test-result/29.4.3: - resolution: {integrity: sha512-Oi4u9NfBolMq9MASPwuWTlC5WvmNRwI4S8YrQg5R5Gi47DYlBe3sh7ILTqi/LGrK1XUE4XY9KZcQJTH1WJCLLA==} + /@jest/test-result/29.5.0: + resolution: {integrity: sha512-fGl4rfitnbfLsrfx1uUpDEESS7zM8JdgZgOCQuxQvL1Sn/I6ijeAVQWGfXI9zb1i9Mzo495cIpVZhA0yr60PkQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/console': 29.4.3 - '@jest/types': 29.4.3 + '@jest/console': 29.5.0 + '@jest/types': 29.5.0 '@types/istanbul-lib-coverage': 2.0.4 collect-v8-coverage: 1.0.1 + dev: false - /@jest/test-sequencer/29.4.3: - resolution: {integrity: sha512-yi/t2nES4GB4G0mjLc0RInCq/cNr9dNwJxcGg8sslajua5Kb4kmozAc+qPLzplhBgfw1vLItbjyHzUN92UXicw==} + /@jest/test-sequencer/29.5.0: + resolution: {integrity: sha512-yPafQEcKjkSfDXyvtgiV4pevSeyuA6MQr6ZIdVkWJly9vkqjnFfcfhRQqpD5whjoU8EORki752xQmjaqoFjzMQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/test-result': 29.4.3 + '@jest/test-result': 29.5.0 graceful-fs: 4.2.10 - jest-haste-map: 29.4.3 + jest-haste-map: 29.5.0 slash: 3.0.0 + dev: false - /@jest/transform/29.4.3: - resolution: {integrity: sha512-8u0+fBGWolDshsFgPQJESkDa72da/EVwvL+II0trN2DR66wMwiQ9/CihaGfHdlLGFzbBZwMykFtxuwFdZqlKwg==} + /@jest/transform/29.5.0: + resolution: {integrity: sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/core': 7.21.0 - '@jest/types': 29.4.3 + '@jest/types': 29.5.0 '@jridgewell/trace-mapping': 0.3.17 babel-plugin-istanbul: 6.1.1 chalk: 4.1.2 convert-source-map: 2.0.0 fast-json-stable-stringify: 2.1.0 graceful-fs: 4.2.10 - jest-haste-map: 29.4.3 + jest-haste-map: 29.5.0 jest-regex-util: 29.4.3 - jest-util: 29.4.3 + jest-util: 29.5.0 micromatch: 4.0.5 pirates: 4.0.5 slash: 3.0.0 write-file-atomic: 4.0.2 transitivePeerDependencies: - supports-color + dev: false - /@jest/types/29.4.3: - resolution: {integrity: sha512-bPYfw8V65v17m2Od1cv44FH+SiKW7w2Xu7trhcdTLUmSv85rfKsP+qXSjO4KGJr4dtPSzl/gvslZBXctf1qGEA==} + /@jest/types/29.5.0: + resolution: {integrity: sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/schemas': 29.4.3 @@ -1000,6 +1030,7 @@ packages: '@types/node': 18.14.6 '@types/yargs': 17.0.22 chalk: 4.1.2 + dev: false /@jridgewell/gen-mapping/0.1.1: resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==} @@ -1263,16 +1294,32 @@ packages: /@sinclair/typebox/0.25.24: resolution: {integrity: sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==} + dev: false /@sinonjs/commons/2.0.0: resolution: {integrity: sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==} dependencies: type-detect: 4.0.8 + dev: false /@sinonjs/fake-timers/10.0.2: resolution: {integrity: sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==} dependencies: '@sinonjs/commons': 2.0.0 + dev: false + + /@tanstack/react-virtual/3.0.0-beta.54_react@18.2.0: + resolution: {integrity: sha512-D1mDMf4UPbrtHRZZriCly5bXTBMhylslm4dhcHqTtDJ6brQcgGmk8YD9JdWBGWfGSWPKoh2x1H3e7eh+hgPXtQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@tanstack/virtual-core': 3.0.0-beta.54 + react: 18.2.0 + dev: false + + /@tanstack/virtual-core/3.0.0-beta.54: + resolution: {integrity: sha512-jtkwqdP2rY2iCCDVAFuaNBH3fiEi29aTn2RhtIoky8DTTiCdc48plpHHreLwmv1PICJ4AJUUESaq3xa8fZH8+g==} + dev: false /@tauri-apps/api/1.2.0: resolution: {integrity: sha512-lsI54KI6HGf7VImuf/T9pnoejfgkNoXveP14pVV7XarrQ46rOejIVJLFqHI9sRReJMGdh2YuCoI3cc/yCWCsrw==} @@ -1384,22 +1431,26 @@ packages: '@types/babel__generator': 7.6.4 '@types/babel__template': 7.4.1 '@types/babel__traverse': 7.18.3 + dev: false /@types/babel__generator/7.6.4: resolution: {integrity: sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==} dependencies: '@babel/types': 7.21.2 + dev: false /@types/babel__template/7.4.1: resolution: {integrity: sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==} dependencies: '@babel/parser': 7.21.2 '@babel/types': 7.21.2 + dev: false /@types/babel__traverse/7.18.3: resolution: {integrity: sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==} dependencies: '@babel/types': 7.21.2 + dev: false /@types/google-protobuf/3.15.6: resolution: {integrity: sha512-pYVNNJ+winC4aek+lZp93sIKxnXt5qMkuKmaqS3WGuTq0Bw1ZDYNBgzG5kkdtwcv+GmYJGo3yEg6z2cKKAiEdw==} @@ -1409,6 +1460,7 @@ packages: resolution: {integrity: sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==} dependencies: '@types/node': 18.14.6 + dev: false /@types/hoist-non-react-statics/3.3.1: resolution: {integrity: sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==} @@ -1422,23 +1474,19 @@ packages: /@types/istanbul-lib-coverage/2.0.4: resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} + dev: false /@types/istanbul-lib-report/3.0.0: resolution: {integrity: sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==} dependencies: '@types/istanbul-lib-coverage': 2.0.4 + dev: false /@types/istanbul-reports/3.0.1: resolution: {integrity: sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==} dependencies: '@types/istanbul-lib-report': 3.0.0 - - /@types/jest/29.4.0: - resolution: {integrity: sha512-VaywcGQ9tPorCX/Jkkni7RWGFfI11whqzs8dvxF41P17Z+z872thvEvlIbznjPJ02kl1HMX3LmLOonsj2n7HeQ==} - dependencies: - expect: 29.4.3 - pretty-format: 29.4.3 - dev: true + dev: false /@types/json-schema/7.0.11: resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} @@ -1448,10 +1496,6 @@ packages: resolution: {integrity: sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==} dev: false - /@types/mocha/10.0.1: - resolution: {integrity: sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==} - dev: true - /@types/node/18.14.6: resolution: {integrity: sha512-93+VvleD3mXwlLI/xASjw0FzKcwzl3OdTCzm1LaRfqgS21gfFtK3zDXM5Op9TeeMsJVOaJ2VRDpT9q4Y3d0AvA==} @@ -1461,6 +1505,7 @@ packages: /@types/prettier/2.7.2: resolution: {integrity: sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==} + dev: false /@types/prop-types/15.7.5: resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} @@ -1498,6 +1543,7 @@ packages: /@types/stack-utils/2.0.1: resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} + dev: false /@types/use-sync-external-store/0.0.3: resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==} @@ -1509,11 +1555,13 @@ packages: /@types/yargs-parser/21.0.0: resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} + dev: false /@types/yargs/17.0.22: resolution: {integrity: sha512-pet5WJ9U8yPVRhkwuEIp5ktAeAqRZOq4UdAyWLWzxbtpyXnzbtLdKiXAjJzi/KLmPGS9wk86lUFWZFN6sISo4g==} dependencies: '@types/yargs-parser': 21.0.0 + dev: false /@typescript-eslint/eslint-plugin/5.54.0_6mj2wypvdnknez7kws2nfdgupi: resolution: {integrity: sha512-+hSN9BdSr629RF02d7mMtXhAJvDTyCbprNYJKrXETlul/Aml6YZwd90XioVbjejQeHbb3R8Dg0CkRgoJDxo8aw==} @@ -1708,6 +1756,7 @@ packages: engines: {node: '>=8'} dependencies: type-fest: 0.21.3 + dev: false /ansi-regex/5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} @@ -1728,6 +1777,7 @@ packages: /ansi-styles/5.2.0: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} + dev: false /anymatch/3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} @@ -1744,6 +1794,7 @@ packages: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} dependencies: sprintf-js: 1.0.3 + dev: false /argparse/2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -1806,22 +1857,23 @@ packages: engines: {node: '>= 0.4'} dev: true - /babel-jest/29.4.3_@babel+core@7.21.0: - resolution: {integrity: sha512-o45Wyn32svZE+LnMVWv/Z4x0SwtLbh4FyGcYtR20kIWd+rdrDZ9Fzq8Ml3MYLD+mZvEdzCjZsCnYZ2jpJyQ+Nw==} + /babel-jest/29.5.0_@babel+core@7.21.0: + resolution: {integrity: sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@babel/core': ^7.8.0 dependencies: '@babel/core': 7.21.0 - '@jest/transform': 29.4.3 + '@jest/transform': 29.5.0 '@types/babel__core': 7.20.0 babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.4.3_@babel+core@7.21.0 + babel-preset-jest: 29.5.0_@babel+core@7.21.0 chalk: 4.1.2 graceful-fs: 4.2.10 slash: 3.0.0 transitivePeerDependencies: - supports-color + dev: false /babel-plugin-istanbul/6.1.1: resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} @@ -1834,15 +1886,17 @@ packages: test-exclude: 6.0.0 transitivePeerDependencies: - supports-color + dev: false - /babel-plugin-jest-hoist/29.4.3: - resolution: {integrity: sha512-mB6q2q3oahKphy5V7CpnNqZOCkxxZ9aokf1eh82Dy3jQmg4xvM1tGrh5y6BQUJh4a3Pj9+eLfwvAZ7VNKg7H8Q==} + /babel-plugin-jest-hoist/29.5.0: + resolution: {integrity: sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/template': 7.20.7 '@babel/types': 7.21.2 '@types/babel__core': 7.20.0 '@types/babel__traverse': 7.18.3 + dev: false /babel-plugin-macros/3.1.0: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} @@ -1871,16 +1925,18 @@ packages: '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.21.0 '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.21.0 '@babel/plugin-syntax-top-level-await': 7.14.5_@babel+core@7.21.0 + dev: false - /babel-preset-jest/29.4.3_@babel+core@7.21.0: - resolution: {integrity: sha512-gWx6COtSuma6n9bw+8/F+2PCXrIgxV/D1TJFnp6OyBK2cxPWg0K9p/sriNYeifKjpUkMViWQ09DSWtzJQRETsw==} + /babel-preset-jest/29.5.0_@babel+core@7.21.0: + resolution: {integrity: sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@babel/core': ^7.0.0 dependencies: '@babel/core': 7.21.0 - babel-plugin-jest-hoist: 29.4.3 + babel-plugin-jest-hoist: 29.5.0 babel-preset-current-node-syntax: 1.0.1_@babel+core@7.21.0 + dev: false /balanced-match/1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1912,20 +1968,15 @@ packages: node-releases: 2.0.10 update-browserslist-db: 1.0.10_browserslist@4.21.5 - /bs-logger/0.2.6: - resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} - engines: {node: '>= 6'} - dependencies: - fast-json-stable-stringify: 2.1.0 - dev: true - /bser/2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} dependencies: node-int64: 0.4.0 + dev: false /buffer-from/1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: false /call-bind/1.0.2: resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} @@ -1946,10 +1997,12 @@ packages: /camelcase/5.3.1: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} + dev: false /camelcase/6.3.0: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} + dev: false /caniuse-lite/1.0.30001460: resolution: {integrity: sha512-Bud7abqjvEjipUkpLs4D7gR0l8hBYBHoa+tGtKJHvT2AYzLp1z7EmVkUT4ERpVUfca8S2HGIVs883D8pUH1ZzQ==} @@ -1972,6 +2025,7 @@ packages: /char-regex/1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} + dev: false /chokidar/3.5.3: resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} @@ -1991,9 +2045,11 @@ packages: /ci-info/3.8.0: resolution: {integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==} engines: {node: '>=8'} + dev: false /cjs-module-lexer/1.2.2: resolution: {integrity: sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==} + dev: false /cliui/8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} @@ -2002,6 +2058,7 @@ packages: string-width: 4.2.3 strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + dev: false /clsx/1.2.1: resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} @@ -2011,9 +2068,11 @@ packages: /co/4.6.0: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + dev: false /collect-v8-coverage/1.0.1: resolution: {integrity: sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==} + dev: false /color-convert/1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -2044,6 +2103,7 @@ packages: /convert-source-map/2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + dev: false /cosmiconfig/7.1.0: resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} @@ -2086,6 +2146,7 @@ packages: /dedent/0.7.0: resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} + dev: false /deep-is/0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -2094,6 +2155,7 @@ packages: /deepmerge/4.3.0: resolution: {integrity: sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og==} engines: {node: '>=0.10.0'} + dev: false /define-properties/1.2.0: resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==} @@ -2110,6 +2172,7 @@ packages: /detect-newline/3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} + dev: false /detective/5.2.1: resolution: {integrity: sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==} @@ -2128,6 +2191,7 @@ packages: /diff-sequences/29.4.3: resolution: {integrity: sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: false /dir-glob/3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} @@ -2172,14 +2236,17 @@ packages: /emittery/0.13.1: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} engines: {node: '>=12'} + dev: false /emoji-regex/8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: false /error-ex/1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} dependencies: is-arrayish: 0.2.1 + dev: false /es-abstract/1.21.1: resolution: {integrity: sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg==} @@ -2285,6 +2352,7 @@ packages: /escape-string-regexp/2.0.0: resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} engines: {node: '>=8'} + dev: false /escape-string-regexp/4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} @@ -2412,6 +2480,7 @@ packages: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true + dev: false /esquery/1.5.0: resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} @@ -2442,6 +2511,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /events/3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + dev: false + /execa/5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -2455,20 +2529,23 @@ packages: onetime: 5.1.2 signal-exit: 3.0.7 strip-final-newline: 2.0.0 + dev: false /exit/0.1.2: resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} engines: {node: '>= 0.8.0'} + dev: false - /expect/29.4.3: - resolution: {integrity: sha512-uC05+Q7eXECFpgDrHdXA4k2rpMyStAYPItEDLyQDo5Ta7fVkJnNA/4zh/OIVkVVNZ1oOK1PipQoyNjuZ6sz6Dg==} + /expect/29.5.0: + resolution: {integrity: sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/expect-utils': 29.4.3 + '@jest/expect-utils': 29.5.0 jest-get-type: 29.4.3 - jest-matcher-utils: 29.4.3 - jest-message-util: 29.4.3 - jest-util: 29.4.3 + jest-matcher-utils: 29.5.0 + jest-message-util: 29.5.0 + jest-util: 29.5.0 + dev: false /fast-deep-equal/3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2502,6 +2579,7 @@ packages: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} dependencies: bser: 2.1.1 + dev: false /file-entry-cache/6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} @@ -2526,6 +2604,7 @@ packages: dependencies: locate-path: 5.0.0 path-exists: 4.0.0 + dev: false /find-up/5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} @@ -2591,6 +2670,7 @@ packages: /get-caller-file/2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + dev: false /get-intrinsic/1.2.0: resolution: {integrity: sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==} @@ -2603,10 +2683,12 @@ packages: /get-package-type/0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} + dev: false /get-stream/6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} + dev: false /get-symbol-description/1.0.0: resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} @@ -2682,6 +2764,7 @@ packages: /graceful-fs/4.2.10: resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + dev: false /grapheme-splitter/1.0.4: resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} @@ -2736,6 +2819,7 @@ packages: /html-escaper/2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + dev: false /html-parse-stringify/3.0.1: resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} @@ -2746,6 +2830,7 @@ packages: /human-signals/2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} + dev: false /i18next-browser-languagedetector/7.0.1: resolution: {integrity: sha512-Pa5kFwaczXJAeHE56CHG2aWzFBMJNUNghf0Pm4SwSrEMps/PTKqW90EYWlIvhuYStf3Sn1K0vw+gH3+TLdkH1g==} @@ -2782,6 +2867,7 @@ packages: dependencies: pkg-dir: 4.2.0 resolve-cwd: 3.0.0 + dev: false /imurmurhash/0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} @@ -2815,6 +2901,7 @@ packages: /is-arrayish/0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + dev: false /is-bigint/1.0.4: resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} @@ -2862,10 +2949,12 @@ packages: /is-fullwidth-code-point/3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + dev: false /is-generator-fn/2.1.0: resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} engines: {node: '>=6'} + dev: false /is-glob/4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} @@ -2925,6 +3014,7 @@ packages: /is-stream/2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} + dev: false /is-string/1.0.7: resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} @@ -2963,6 +3053,7 @@ packages: /istanbul-lib-coverage/3.2.0: resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==} engines: {node: '>=8'} + dev: false /istanbul-lib-instrument/5.2.1: resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} @@ -2975,6 +3066,7 @@ packages: semver: 6.3.0 transitivePeerDependencies: - supports-color + dev: false /istanbul-lib-report/3.0.0: resolution: {integrity: sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==} @@ -2983,6 +3075,7 @@ packages: istanbul-lib-coverage: 3.2.0 make-dir: 3.1.0 supports-color: 7.2.0 + dev: false /istanbul-lib-source-maps/4.0.1: resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} @@ -2993,6 +3086,7 @@ packages: source-map: 0.6.1 transitivePeerDependencies: - supports-color + dev: false /istanbul-reports/3.1.5: resolution: {integrity: sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==} @@ -3000,42 +3094,46 @@ packages: dependencies: html-escaper: 2.0.2 istanbul-lib-report: 3.0.0 + dev: false - /jest-changed-files/29.4.3: - resolution: {integrity: sha512-Vn5cLuWuwmi2GNNbokPOEcvrXGSGrqVnPEZV7rC6P7ck07Dyw9RFnvWglnupSh+hGys0ajGtw/bc2ZgweljQoQ==} + /jest-changed-files/29.5.0: + resolution: {integrity: sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: execa: 5.1.1 p-limit: 3.1.0 + dev: false - /jest-circus/29.4.3: - resolution: {integrity: sha512-Vw/bVvcexmdJ7MLmgdT3ZjkJ3LKu8IlpefYokxiqoZy6OCQ2VAm6Vk3t/qHiAGUXbdbJKJWnc8gH3ypTbB/OBw==} + /jest-circus/29.5.0: + resolution: {integrity: sha512-gq/ongqeQKAplVxqJmbeUOJJKkW3dDNPY8PjhJ5G0lBRvu0e3EWGxGy5cI4LAGA7gV2UHCtWBI4EMXK8c9nQKA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 29.4.3 - '@jest/expect': 29.4.3 - '@jest/test-result': 29.4.3 - '@jest/types': 29.4.3 + '@jest/environment': 29.5.0 + '@jest/expect': 29.5.0 + '@jest/test-result': 29.5.0 + '@jest/types': 29.5.0 '@types/node': 18.14.6 chalk: 4.1.2 co: 4.6.0 dedent: 0.7.0 is-generator-fn: 2.1.0 - jest-each: 29.4.3 - jest-matcher-utils: 29.4.3 - jest-message-util: 29.4.3 - jest-runtime: 29.4.3 - jest-snapshot: 29.4.3 - jest-util: 29.4.3 + jest-each: 29.5.0 + jest-matcher-utils: 29.5.0 + jest-message-util: 29.5.0 + jest-runtime: 29.5.0 + jest-snapshot: 29.5.0 + jest-util: 29.5.0 p-limit: 3.1.0 - pretty-format: 29.4.3 + pretty-format: 29.5.0 + pure-rand: 6.0.1 slash: 3.0.0 stack-utils: 2.0.6 transitivePeerDependencies: - supports-color + dev: false - /jest-cli/29.4.3_@types+node@18.14.6: - resolution: {integrity: sha512-PiiAPuFNfWWolCE6t3ZrDXQc6OsAuM3/tVW0u27UWc1KE+n/HSn5dSE6B2juqN7WP+PP0jAcnKtGmI4u8GMYCg==} + /jest-cli/29.5.0_@types+node@18.14.6: + resolution: {integrity: sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true peerDependencies: @@ -3044,25 +3142,26 @@ packages: node-notifier: optional: true dependencies: - '@jest/core': 29.4.3 - '@jest/test-result': 29.4.3 - '@jest/types': 29.4.3 + '@jest/core': 29.5.0 + '@jest/test-result': 29.5.0 + '@jest/types': 29.5.0 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.10 import-local: 3.1.0 - jest-config: 29.4.3_@types+node@18.14.6 - jest-util: 29.4.3 - jest-validate: 29.4.3 + jest-config: 29.5.0_@types+node@18.14.6 + jest-util: 29.5.0 + jest-validate: 29.5.0 prompts: 2.4.2 yargs: 17.7.1 transitivePeerDependencies: - '@types/node' - supports-color - ts-node + dev: false - /jest-config/29.4.3_@types+node@18.14.6: - resolution: {integrity: sha512-eCIpqhGnIjdUCXGtLhz4gdDoxKSWXKjzNcc5r+0S1GKOp2fwOipx5mRcwa9GB/ArsxJ1jlj2lmlD9bZAsBxaWQ==} + /jest-config/29.5.0_@types+node@18.14.6: + resolution: {integrity: sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@types/node': '*' @@ -3074,128 +3173,139 @@ packages: optional: true dependencies: '@babel/core': 7.21.0 - '@jest/test-sequencer': 29.4.3 - '@jest/types': 29.4.3 + '@jest/test-sequencer': 29.5.0 + '@jest/types': 29.5.0 '@types/node': 18.14.6 - babel-jest: 29.4.3_@babel+core@7.21.0 + babel-jest: 29.5.0_@babel+core@7.21.0 chalk: 4.1.2 ci-info: 3.8.0 deepmerge: 4.3.0 glob: 7.2.3 graceful-fs: 4.2.10 - jest-circus: 29.4.3 - jest-environment-node: 29.4.3 + jest-circus: 29.5.0 + jest-environment-node: 29.5.0 jest-get-type: 29.4.3 jest-regex-util: 29.4.3 - jest-resolve: 29.4.3 - jest-runner: 29.4.3 - jest-util: 29.4.3 - jest-validate: 29.4.3 + jest-resolve: 29.5.0 + jest-runner: 29.5.0 + jest-util: 29.5.0 + jest-validate: 29.5.0 micromatch: 4.0.5 parse-json: 5.2.0 - pretty-format: 29.4.3 + pretty-format: 29.5.0 slash: 3.0.0 strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color + dev: false - /jest-diff/29.4.3: - resolution: {integrity: sha512-YB+ocenx7FZ3T5O9lMVMeLYV4265socJKtkwgk/6YUz/VsEzYDkiMuMhWzZmxm3wDRQvayJu/PjkjjSkjoHsCA==} + /jest-diff/29.5.0: + resolution: {integrity: sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: chalk: 4.1.2 diff-sequences: 29.4.3 jest-get-type: 29.4.3 - pretty-format: 29.4.3 + pretty-format: 29.5.0 + dev: false /jest-docblock/29.4.3: resolution: {integrity: sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: detect-newline: 3.1.0 + dev: false - /jest-each/29.4.3: - resolution: {integrity: sha512-1ElHNAnKcbJb/b+L+7j0/w7bDvljw4gTv1wL9fYOczeJrbTbkMGQ5iQPFJ3eFQH19VPTx1IyfePdqSpePKss7Q==} + /jest-each/29.5.0: + resolution: {integrity: sha512-HM5kIJ1BTnVt+DQZ2ALp3rzXEl+g726csObrW/jpEGl+CDSSQpOJJX2KE/vEg8cxcMXdyEPu6U4QX5eruQv5hA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.4.3 + '@jest/types': 29.5.0 chalk: 4.1.2 jest-get-type: 29.4.3 - jest-util: 29.4.3 - pretty-format: 29.4.3 + jest-util: 29.5.0 + pretty-format: 29.5.0 + dev: false - /jest-environment-node/29.4.3: - resolution: {integrity: sha512-gAiEnSKF104fsGDXNkwk49jD/0N0Bqu2K9+aMQXA6avzsA9H3Fiv1PW2D+gzbOSR705bWd2wJZRFEFpV0tXISg==} + /jest-environment-node/29.5.0: + resolution: {integrity: sha512-ExxuIK/+yQ+6PRGaHkKewYtg6hto2uGCgvKdb2nfJfKXgZ17DfXjvbZ+jA1Qt9A8EQSfPnt5FKIfnOO3u1h9qw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 29.4.3 - '@jest/fake-timers': 29.4.3 - '@jest/types': 29.4.3 + '@jest/environment': 29.5.0 + '@jest/fake-timers': 29.5.0 + '@jest/types': 29.5.0 '@types/node': 18.14.6 - jest-mock: 29.4.3 - jest-util: 29.4.3 + jest-mock: 29.5.0 + jest-util: 29.5.0 + dev: false /jest-get-type/29.4.3: resolution: {integrity: sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: false - /jest-haste-map/29.4.3: - resolution: {integrity: sha512-eZIgAS8tvm5IZMtKlR8Y+feEOMfo2pSQkmNbufdbMzMSn9nitgGxF1waM/+LbryO3OkMcKS98SUb+j/cQxp/vQ==} + /jest-haste-map/29.5.0: + resolution: {integrity: sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.4.3 + '@jest/types': 29.5.0 '@types/graceful-fs': 4.1.6 '@types/node': 18.14.6 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.10 jest-regex-util: 29.4.3 - jest-util: 29.4.3 - jest-worker: 29.4.3 + jest-util: 29.5.0 + jest-worker: 29.5.0 micromatch: 4.0.5 walker: 1.0.8 optionalDependencies: fsevents: 2.3.2 + dev: false - /jest-leak-detector/29.4.3: - resolution: {integrity: sha512-9yw4VC1v2NspMMeV3daQ1yXPNxMgCzwq9BocCwYrRgXe4uaEJPAN0ZK37nFBhcy3cUwEVstFecFLaTHpF7NiGA==} + /jest-leak-detector/29.5.0: + resolution: {integrity: sha512-u9YdeeVnghBUtpN5mVxjID7KbkKE1QU4f6uUwuxiY0vYRi9BUCLKlPEZfDGR67ofdFmDz9oPAy2G92Ujrntmow==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: jest-get-type: 29.4.3 - pretty-format: 29.4.3 + pretty-format: 29.5.0 + dev: false - /jest-matcher-utils/29.4.3: - resolution: {integrity: sha512-TTciiXEONycZ03h6R6pYiZlSkvYgT0l8aa49z/DLSGYjex4orMUcafuLXYyyEDWB1RKglq00jzwY00Ei7yFNVg==} + /jest-matcher-utils/29.5.0: + resolution: {integrity: sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: chalk: 4.1.2 - jest-diff: 29.4.3 + jest-diff: 29.5.0 jest-get-type: 29.4.3 - pretty-format: 29.4.3 + pretty-format: 29.5.0 + dev: false - /jest-message-util/29.4.3: - resolution: {integrity: sha512-1Y8Zd4ZCN7o/QnWdMmT76If8LuDv23Z1DRovBj/vcSFNlGCJGoO8D1nJDw1AdyAGUk0myDLFGN5RbNeJyCRGCw==} + /jest-message-util/29.5.0: + resolution: {integrity: sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/code-frame': 7.18.6 - '@jest/types': 29.4.3 + '@jest/types': 29.5.0 '@types/stack-utils': 2.0.1 chalk: 4.1.2 graceful-fs: 4.2.10 micromatch: 4.0.5 - pretty-format: 29.4.3 + pretty-format: 29.5.0 slash: 3.0.0 stack-utils: 2.0.6 + dev: false - /jest-mock/29.4.3: - resolution: {integrity: sha512-LjFgMg+xed9BdkPMyIJh+r3KeHt1klXPJYBULXVVAkbTaaKjPX1o1uVCAZADMEp/kOxGTwy/Ot8XbvgItOrHEg==} + /jest-mock/29.5.0: + resolution: {integrity: sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.4.3 + '@jest/types': 29.5.0 '@types/node': 18.14.6 - jest-util: 29.4.3 + jest-util: 29.5.0 + dev: false - /jest-pnp-resolver/1.2.3_jest-resolve@29.4.3: + /jest-pnp-resolver/1.2.3_jest-resolve@29.5.0: resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} engines: {node: '>=6'} peerDependencies: @@ -3204,94 +3314,100 @@ packages: jest-resolve: optional: true dependencies: - jest-resolve: 29.4.3 + jest-resolve: 29.5.0 + dev: false /jest-regex-util/29.4.3: resolution: {integrity: sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: false - /jest-resolve-dependencies/29.4.3: - resolution: {integrity: sha512-uvKMZAQ3nmXLH7O8WAOhS5l0iWyT3WmnJBdmIHiV5tBbdaDZ1wqtNX04FONGoaFvSOSHBJxnwAVnSn1WHdGVaw==} + /jest-resolve-dependencies/29.5.0: + resolution: {integrity: sha512-sjV3GFr0hDJMBpYeUuGduP+YeCRbd7S/ck6IvL3kQ9cpySYKqcqhdLLC2rFwrcL7tz5vYibomBrsFYWkIGGjOg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: jest-regex-util: 29.4.3 - jest-snapshot: 29.4.3 + jest-snapshot: 29.5.0 transitivePeerDependencies: - supports-color + dev: false - /jest-resolve/29.4.3: - resolution: {integrity: sha512-GPokE1tzguRyT7dkxBim4wSx6E45S3bOQ7ZdKEG+Qj0Oac9+6AwJPCk0TZh5Vu0xzeX4afpb+eDmgbmZFFwpOw==} + /jest-resolve/29.5.0: + resolution: {integrity: sha512-1TzxJ37FQq7J10jPtQjcc+MkCkE3GBpBecsSUWJ0qZNJpmg6m0D9/7II03yJulm3H/fvVjgqLh/k2eYg+ui52w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: chalk: 4.1.2 graceful-fs: 4.2.10 - jest-haste-map: 29.4.3 - jest-pnp-resolver: 1.2.3_jest-resolve@29.4.3 - jest-util: 29.4.3 - jest-validate: 29.4.3 + jest-haste-map: 29.5.0 + jest-pnp-resolver: 1.2.3_jest-resolve@29.5.0 + jest-util: 29.5.0 + jest-validate: 29.5.0 resolve: 1.22.1 resolve.exports: 2.0.0 slash: 3.0.0 + dev: false - /jest-runner/29.4.3: - resolution: {integrity: sha512-GWPTEiGmtHZv1KKeWlTX9SIFuK19uLXlRQU43ceOQ2hIfA5yPEJC7AMkvFKpdCHx6pNEdOD+2+8zbniEi3v3gA==} + /jest-runner/29.5.0: + resolution: {integrity: sha512-m7b6ypERhFghJsslMLhydaXBiLf7+jXy8FwGRHO3BGV1mcQpPbwiqiKUR2zU2NJuNeMenJmlFZCsIqzJCTeGLQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/console': 29.4.3 - '@jest/environment': 29.4.3 - '@jest/test-result': 29.4.3 - '@jest/transform': 29.4.3 - '@jest/types': 29.4.3 + '@jest/console': 29.5.0 + '@jest/environment': 29.5.0 + '@jest/test-result': 29.5.0 + '@jest/transform': 29.5.0 + '@jest/types': 29.5.0 '@types/node': 18.14.6 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.10 jest-docblock: 29.4.3 - jest-environment-node: 29.4.3 - jest-haste-map: 29.4.3 - jest-leak-detector: 29.4.3 - jest-message-util: 29.4.3 - jest-resolve: 29.4.3 - jest-runtime: 29.4.3 - jest-util: 29.4.3 - jest-watcher: 29.4.3 - jest-worker: 29.4.3 + jest-environment-node: 29.5.0 + jest-haste-map: 29.5.0 + jest-leak-detector: 29.5.0 + jest-message-util: 29.5.0 + jest-resolve: 29.5.0 + jest-runtime: 29.5.0 + jest-util: 29.5.0 + jest-watcher: 29.5.0 + jest-worker: 29.5.0 p-limit: 3.1.0 source-map-support: 0.5.13 transitivePeerDependencies: - supports-color + dev: false - /jest-runtime/29.4.3: - resolution: {integrity: sha512-F5bHvxSH+LvLV24vVB3L8K467dt3y3dio6V3W89dUz9nzvTpqd/HcT9zfYKL2aZPvD63vQFgLvaUX/UpUhrP6Q==} + /jest-runtime/29.5.0: + resolution: {integrity: sha512-1Hr6Hh7bAgXQP+pln3homOiEZtCDZFqwmle7Ew2j8OlbkIu6uE3Y/etJQG8MLQs3Zy90xrp2C0BRrtPHG4zryw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 29.4.3 - '@jest/fake-timers': 29.4.3 - '@jest/globals': 29.4.3 + '@jest/environment': 29.5.0 + '@jest/fake-timers': 29.5.0 + '@jest/globals': 29.5.0 '@jest/source-map': 29.4.3 - '@jest/test-result': 29.4.3 - '@jest/transform': 29.4.3 - '@jest/types': 29.4.3 + '@jest/test-result': 29.5.0 + '@jest/transform': 29.5.0 + '@jest/types': 29.5.0 '@types/node': 18.14.6 chalk: 4.1.2 cjs-module-lexer: 1.2.2 collect-v8-coverage: 1.0.1 glob: 7.2.3 graceful-fs: 4.2.10 - jest-haste-map: 29.4.3 - jest-message-util: 29.4.3 - jest-mock: 29.4.3 + jest-haste-map: 29.5.0 + jest-message-util: 29.5.0 + jest-mock: 29.5.0 jest-regex-util: 29.4.3 - jest-resolve: 29.4.3 - jest-snapshot: 29.4.3 - jest-util: 29.4.3 + jest-resolve: 29.5.0 + jest-snapshot: 29.5.0 + jest-util: 29.5.0 slash: 3.0.0 strip-bom: 4.0.0 transitivePeerDependencies: - supports-color + dev: false - /jest-snapshot/29.4.3: - resolution: {integrity: sha512-NGlsqL0jLPDW91dz304QTM/SNO99lpcSYYAjNiX0Ou+sSGgkanKBcSjCfp/pqmiiO1nQaOyLp6XQddAzRcx3Xw==} + /jest-snapshot/29.5.0: + resolution: {integrity: sha512-x7Wolra5V0tt3wRs3/ts3S6ciSQVypgGQlJpz2rsdQYoUKxMxPNaoHMGJN6qAuPJqS+2iQ1ZUn5kl7HCyls84g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/core': 7.21.0 @@ -3300,73 +3416,77 @@ packages: '@babel/plugin-syntax-typescript': 7.20.0_@babel+core@7.21.0 '@babel/traverse': 7.21.2 '@babel/types': 7.21.2 - '@jest/expect-utils': 29.4.3 - '@jest/transform': 29.4.3 - '@jest/types': 29.4.3 + '@jest/expect-utils': 29.5.0 + '@jest/transform': 29.5.0 + '@jest/types': 29.5.0 '@types/babel__traverse': 7.18.3 '@types/prettier': 2.7.2 babel-preset-current-node-syntax: 1.0.1_@babel+core@7.21.0 chalk: 4.1.2 - expect: 29.4.3 + expect: 29.5.0 graceful-fs: 4.2.10 - jest-diff: 29.4.3 + jest-diff: 29.5.0 jest-get-type: 29.4.3 - jest-haste-map: 29.4.3 - jest-matcher-utils: 29.4.3 - jest-message-util: 29.4.3 - jest-util: 29.4.3 + jest-matcher-utils: 29.5.0 + jest-message-util: 29.5.0 + jest-util: 29.5.0 natural-compare: 1.4.0 - pretty-format: 29.4.3 + pretty-format: 29.5.0 semver: 7.3.8 transitivePeerDependencies: - supports-color + dev: false - /jest-util/29.4.3: - resolution: {integrity: sha512-ToSGORAz4SSSoqxDSylWX8JzkOQR7zoBtNRsA7e+1WUX5F8jrOwaNpuh1YfJHJKDHXLHmObv5eOjejUd+/Ws+Q==} + /jest-util/29.5.0: + resolution: {integrity: sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.4.3 + '@jest/types': 29.5.0 '@types/node': 18.14.6 chalk: 4.1.2 ci-info: 3.8.0 graceful-fs: 4.2.10 picomatch: 2.3.1 + dev: false - /jest-validate/29.4.3: - resolution: {integrity: sha512-J3u5v7aPQoXPzaar6GndAVhdQcZr/3osWSgTeKg5v574I9ybX/dTyH0AJFb5XgXIB7faVhf+rS7t4p3lL9qFaw==} + /jest-validate/29.5.0: + resolution: {integrity: sha512-pC26etNIi+y3HV8A+tUGr/lph9B18GnzSRAkPaaZJIE1eFdiYm6/CewuiJQ8/RlfHd1u/8Ioi8/sJ+CmbA+zAQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.4.3 + '@jest/types': 29.5.0 camelcase: 6.3.0 chalk: 4.1.2 jest-get-type: 29.4.3 leven: 3.1.0 - pretty-format: 29.4.3 + pretty-format: 29.5.0 + dev: false - /jest-watcher/29.4.3: - resolution: {integrity: sha512-zwlXH3DN3iksoIZNk73etl1HzKyi5FuQdYLnkQKm5BW4n8HpoG59xSwpVdFrnh60iRRaRBGw0gcymIxjJENPcA==} + /jest-watcher/29.5.0: + resolution: {integrity: sha512-KmTojKcapuqYrKDpRwfqcQ3zjMlwu27SYext9pt4GlF5FUgB+7XE1mcCnSm6a4uUpFyQIkb6ZhzZvHl+jiBCiA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/test-result': 29.4.3 - '@jest/types': 29.4.3 + '@jest/test-result': 29.5.0 + '@jest/types': 29.5.0 '@types/node': 18.14.6 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 - jest-util: 29.4.3 + jest-util: 29.5.0 string-length: 4.0.2 + dev: false - /jest-worker/29.4.3: - resolution: {integrity: sha512-GLHN/GTAAMEy5BFdvpUfzr9Dr80zQqBrh0fz1mtRMe05hqP45+HfQltu7oTBfduD0UeZs09d+maFtFYAXFWvAA==} + /jest-worker/29.5.0: + resolution: {integrity: sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@types/node': 18.14.6 - jest-util: 29.4.3 + jest-util: 29.5.0 merge-stream: 2.0.0 supports-color: 8.1.1 + dev: false - /jest/29.4.3_@types+node@18.14.6: - resolution: {integrity: sha512-XvK65feuEFGZT8OO0fB/QAQS+LGHvQpaadkH5p47/j3Ocqq3xf2pK9R+G0GzgfuhXVxEv76qCOOcMb5efLk6PA==} + /jest/29.5.0_@types+node@18.14.6: + resolution: {integrity: sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true peerDependencies: @@ -3375,14 +3495,15 @@ packages: node-notifier: optional: true dependencies: - '@jest/core': 29.4.3 - '@jest/types': 29.4.3 + '@jest/core': 29.5.0 + '@jest/types': 29.5.0 import-local: 3.1.0 - jest-cli: 29.4.3_@types+node@18.14.6 + jest-cli: 29.5.0_@types+node@18.14.6 transitivePeerDependencies: - '@types/node' - supports-color - ts-node + dev: false /js-sdsl/4.3.0: resolution: {integrity: sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==} @@ -3397,6 +3518,7 @@ packages: dependencies: argparse: 1.0.10 esprima: 4.0.1 + dev: false /js-yaml/4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} @@ -3412,6 +3534,7 @@ packages: /json-parse-even-better-errors/2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + dev: false /json-schema-traverse/0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -3437,10 +3560,12 @@ packages: /kleur/3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} + dev: false /leven/3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} + dev: false /levn/0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} @@ -3457,12 +3582,14 @@ packages: /lines-and-columns/1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + dev: false /locate-path/5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} dependencies: p-locate: 4.1.0 + dev: false /locate-path/6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} @@ -3471,10 +3598,6 @@ packages: p-locate: 5.0.0 dev: true - /lodash.memoize/4.1.2: - resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} - dev: true - /lodash.merge/4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true @@ -3512,18 +3635,17 @@ packages: engines: {node: '>=8'} dependencies: semver: 6.3.0 - - /make-error/1.3.6: - resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - dev: true + dev: false /makeerror/1.0.12: resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} dependencies: tmpl: 1.0.5 + dev: false /merge-stream/2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: false /merge2/1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} @@ -3540,6 +3662,7 @@ packages: /mimic-fn/2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} + dev: false /minimatch/3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -3574,6 +3697,7 @@ packages: /node-int64/0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + dev: false /node-releases/2.0.10: resolution: {integrity: sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==} @@ -3592,6 +3716,7 @@ packages: engines: {node: '>=8'} dependencies: path-key: 3.1.1 + dev: false /object-assign/4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} @@ -3665,6 +3790,7 @@ packages: engines: {node: '>=6'} dependencies: mimic-fn: 2.1.0 + dev: false /optionator/0.9.1: resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==} @@ -3683,6 +3809,7 @@ packages: engines: {node: '>=6'} dependencies: p-try: 2.2.0 + dev: false /p-limit/3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} @@ -3695,6 +3822,7 @@ packages: engines: {node: '>=8'} dependencies: p-limit: 2.3.0 + dev: false /p-locate/5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} @@ -3706,6 +3834,7 @@ packages: /p-try/2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + dev: false /parent-module/1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} @@ -3721,6 +3850,7 @@ packages: error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + dev: false /path-exists/4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} @@ -3756,12 +3886,14 @@ packages: /pirates/4.0.5: resolution: {integrity: sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==} engines: {node: '>= 6'} + dev: false /pkg-dir/4.2.0: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} dependencies: find-up: 4.1.0 + dev: false /postcss-import/14.1.0_postcss@8.4.21: resolution: {integrity: sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==} @@ -3899,13 +4031,14 @@ packages: hasBin: true dev: true - /pretty-format/29.4.3: - resolution: {integrity: sha512-cvpcHTc42lcsvOOAzd3XuNWTcvk1Jmnzqeu+WsOuiPmxUJTnkbAcFNsRKvEpBEUFVUgy/GTZLulZDcDEi+CIlA==} + /pretty-format/29.5.0: + resolution: {integrity: sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/schemas': 29.4.3 ansi-styles: 5.2.0 react-is: 18.2.0 + dev: false /prompts/2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} @@ -3913,6 +4046,7 @@ packages: dependencies: kleur: 3.0.3 sisteransi: 1.0.5 + dev: false /prop-types/15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -3926,6 +4060,10 @@ packages: engines: {node: '>=6'} dev: true + /pure-rand/6.0.1: + resolution: {integrity: sha512-t+x1zEHDjBwkDGY5v5ApnZ/utcd4XYDiJsaQQoptTXgUXX95sDg1elCdJghzicm7n2mbCBJ3uYWr6M22SO19rg==} + dev: false + /queue-microtask/1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true @@ -3980,6 +4118,7 @@ packages: /react-is/18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + dev: false /react-redux/8.0.5_ctrls2ti7t7iutxbwkm5ipogyy: resolution: {integrity: sha512-Q2f6fCKxPFpkXt1qNRZdEDLlScsDWyrgSj0mliK59qU6W5gvBiKkdMEG2lJzhd1rCctf0hb6EtePPLZ2e0m1uw==} @@ -4122,6 +4261,7 @@ packages: /require-directory/2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + dev: false /reselect/4.1.7: resolution: {integrity: sha512-Zu1xbUt3/OPwsXL46hvOOoQrap2azE7ZQbokq61BQfiXvhewsKDwhMeZjTX9sX0nvw1t/U5Audyn1I9P/m9z0A==} @@ -4132,6 +4272,7 @@ packages: engines: {node: '>=8'} dependencies: resolve-from: 5.0.0 + dev: false /resolve-from/4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} @@ -4140,10 +4281,12 @@ packages: /resolve-from/5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} + dev: false /resolve.exports/2.0.0: resolution: {integrity: sha512-6K/gDlqgQscOlg9fSRpWstA8sYe8rbELsSTNpx+3kTrsVCzvSl0zIvRErM7fdl9ERWDsKnrLnwB+Ne89918XOg==} engines: {node: '>=10'} + dev: false /resolve/1.22.1: resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} @@ -4245,9 +4388,11 @@ packages: /signal-exit/3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + dev: false /sisteransi/1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + dev: false /slash/3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} @@ -4292,6 +4437,7 @@ packages: dependencies: buffer-from: 1.1.2 source-map: 0.6.1 + dev: false /source-map/0.5.7: resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} @@ -4301,15 +4447,18 @@ packages: /source-map/0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + dev: false /sprintf-js/1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + dev: false /stack-utils/2.0.6: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} dependencies: escape-string-regexp: 2.0.0 + dev: false /string-length/4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} @@ -4317,6 +4466,7 @@ packages: dependencies: char-regex: 1.0.2 strip-ansi: 6.0.1 + dev: false /string-width/4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} @@ -4325,6 +4475,7 @@ packages: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + dev: false /string.prototype.matchall/4.0.8: resolution: {integrity: sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==} @@ -4364,10 +4515,12 @@ packages: /strip-bom/4.0.0: resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} engines: {node: '>=8'} + dev: false /strip-final-newline/2.0.0: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} + dev: false /strip-json-comments/3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} @@ -4394,6 +4547,7 @@ packages: engines: {node: '>=10'} dependencies: has-flag: 4.0.0 + dev: false /supports-preserve-symlinks-flag/1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} @@ -4440,6 +4594,7 @@ packages: '@istanbuljs/schema': 0.1.3 glob: 7.2.3 minimatch: 3.1.2 + dev: false /text-table/0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -4455,6 +4610,7 @@ packages: /tmpl/1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + dev: false /to-fast-properties/2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} @@ -4466,39 +4622,6 @@ packages: dependencies: is-number: 7.0.0 - /ts-jest/29.0.5_orzzknleilowtsz34rkaotjvzm: - resolution: {integrity: sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - '@babel/core': '>=7.0.0-beta.0 <8' - '@jest/types': ^29.0.0 - babel-jest: ^29.0.0 - esbuild: '*' - jest: ^29.0.0 - typescript: '>=4.3' - peerDependenciesMeta: - '@babel/core': - optional: true - '@jest/types': - optional: true - babel-jest: - optional: true - esbuild: - optional: true - dependencies: - bs-logger: 0.2.6 - fast-json-stable-stringify: 2.1.0 - jest: 29.4.3_@types+node@18.14.6 - jest-util: 29.4.3 - json5: 2.2.3 - lodash.memoize: 4.1.2 - make-error: 1.3.6 - semver: 7.3.8 - typescript: 4.9.5 - yargs-parser: 21.1.1 - dev: true - /ts-results/3.3.0: resolution: {integrity: sha512-FWqxGX2NHp5oCyaMd96o2y2uMQmSu8Dey6kvyuFdRJ2AzfmWo3kWa4UsPlCGlfQ/qu03m09ZZtppMoY8EMHuiA==} dev: false @@ -4531,6 +4654,7 @@ packages: /type-detect/4.0.8: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} + dev: false /type-fest/0.20.2: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} @@ -4540,6 +4664,7 @@ packages: /type-fest/0.21.3: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} + dev: false /typed-array-length/1.0.4: resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} @@ -4555,6 +4680,11 @@ packages: hasBin: true dev: true + /ulid/2.3.0: + resolution: {integrity: sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==} + hasBin: true + dev: false + /unbox-primitive/1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} dependencies: @@ -4603,6 +4733,7 @@ packages: '@jridgewell/trace-mapping': 0.3.17 '@types/istanbul-lib-coverage': 2.0.4 convert-source-map: 1.9.0 + dev: false /vite/4.1.4_@types+node@18.14.6: resolution: {integrity: sha512-3knk/HsbSTKEin43zHu7jTwYWv81f8kgAL99G5NWBcA1LKvtvcVAC4JjBH1arBunO9kQka+1oGbrMKOjk4ZrBg==} @@ -4647,6 +4778,7 @@ packages: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} dependencies: makeerror: 1.0.12 + dev: false /which-boxed-primitive/1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} @@ -4689,6 +4821,7 @@ packages: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 + dev: false /wrappy/1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -4699,6 +4832,7 @@ packages: dependencies: imurmurhash: 0.1.4 signal-exit: 3.0.7 + dev: false /xtend/4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} @@ -4708,6 +4842,7 @@ packages: /y18n/5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} + dev: false /yallist/3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -4722,6 +4857,7 @@ packages: /yargs-parser/21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + dev: false /yargs/17.7.1: resolution: {integrity: sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==} @@ -4734,6 +4870,7 @@ packages: string-width: 4.2.3 y18n: 5.0.8 yargs-parser: 21.1.1 + dev: false /yocto-queue/0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/block.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/block.ts deleted file mode 100644 index 8fb8610b77..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/block.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { BlockInterface, BlockType } from '$app/interfaces/index'; - - -export class BlockDataManager { - private head: BlockInterface | null = null; - constructor(id: string, private map: Record> | null) { - if (!map) return; - this.head = map[id]; - } - - setBlocksMap = (id: string, map: Record>) => { - this.map = map; - this.head = map[id]; - } - - /** - * get block data - * @param blockId string - * @returns Block - */ - getBlock = (blockId: string) => { - return this.map?.[blockId] || null; - } - - destroy() { - this.map = null; - } -} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/index.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/index.ts new file mode 100644 index 0000000000..de42c3c373 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/index.ts @@ -0,0 +1,71 @@ +import { BaseEditor, BaseSelection, Descendant } from "slate"; +import { TreeNode } from '$app/block_editor/view/tree_node'; +import { Operation } from "$app/block_editor/core/operation"; +import { TextBlockSelectionManager } from './text_selection'; + +export class TextBlockManager { + public selectionManager: TextBlockSelectionManager; + constructor(private operation: Operation) { + this.selectionManager = new TextBlockSelectionManager(); + } + + setSelection(node: TreeNode, selection: BaseSelection) { + // console.log(node.id, selection); + this.selectionManager.setSelection(node.id, selection) + } + + update(node: TreeNode, path: string[], data: Descendant[]) { + this.operation.updateNode(node.id, path, data); + } + + splitNode(node: TreeNode, editor: BaseEditor) { + const focus = editor.selection?.focus; + const path = focus?.path || [0, editor.children.length - 1]; + const offset = focus?.offset || 0; + const parentIndex = path[0]; + const index = path[1]; + const editorNode = editor.children[parentIndex]; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const children: { [key: string]: boolean | string; text: string }[] = editorNode.children; + const retainItems = children.filter((_: any, i: number) => i < index); + const splitItem: { [key: string]: boolean | string } = children[index]; + const text = splitItem.text.toString(); + const prevText = text.substring(0, offset); + const afterText = text.substring(offset); + retainItems.push({ + ...splitItem, + text: prevText + }); + + const removeItems = children.filter((_: any, i: number) => i > index); + + const data = { + type: node.type, + data: { + ...node.data, + content: [ + { + ...splitItem, + text: afterText + }, + ...removeItems + ] + } + }; + + const newBlock = this.operation.splitNode(node.id, { + path: ['data', 'content'], + value: retainItems, + }, data); + newBlock && this.selectionManager.focusStart(newBlock.id); + } + + destroy() { + this.selectionManager.destroy(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + this.operation = null; + } + +} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/text_selection.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/text_selection.ts new file mode 100644 index 0000000000..b25d7f6268 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/block_editor/blocks/text_block/text_selection.ts @@ -0,0 +1,35 @@ +export class TextBlockSelectionManager { + private focusId = ''; + private selection?: any; + + getFocusSelection() { + return { + focusId: this.focusId, + selection: this.selection + } + } + + focusStart(blockId: string) { + this.focusId = blockId; + this.setSelection(blockId, { + focus: { + path: [0, 0], + offset: 0, + }, + anchor: { + path: [0, 0], + offset: 0, + }, + }) + } + + setSelection(blockId: string, selection: any) { + this.focusId = blockId; + this.selection = selection; + } + + destroy() { + this.focusId = ''; + this.selection = undefined; + } +} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block.ts new file mode 100644 index 0000000000..c550213daa --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block.ts @@ -0,0 +1,107 @@ +import { BlockType, BlockData } from '$app/interfaces/index'; +import { generateBlockId } from '$app/utils/block'; + +/** + * Represents a single block of content in a document. + */ +export class Block { + id: string; + type: T; + data: BlockData; + parent: Block | null = null; // Pointer to the parent block + prev: Block | null = null; // Pointer to the previous sibling block + next: Block | null = null; // Pointer to the next sibling block + firstChild: Block | null = null; // Pointer to the first child block + + constructor(id: string, type: T, data: BlockData) { + this.id = id; + this.type = type; + this.data = data; + } + + /** + * Adds a new child block to the beginning of the current block's children list. + * + * @param {Object} content - The content of the new block, including its type and data. + * @param {string} content.type - The type of the new block. + * @param {Object} content.data - The data associated with the new block. + * @returns {Block} The newly created child block. + */ + prependChild(content: { type: T, data: BlockData }): Block | null { + const id = generateBlockId(); + const newBlock = new Block(id, content.type, content.data); + newBlock.reposition(this, null); + return newBlock; + } + + /** + * Add a new sibling block after this block. + * + * @param content The type and data for the new sibling block. + * @returns The newly created sibling block. + */ + addSibling(content: { type: T, data: BlockData }): Block | null { + const id = generateBlockId(); + const newBlock = new Block(id, content.type, content.data); + newBlock.reposition(this.parent, this); + return newBlock; + } + + /** + * Remove this block and its descendants from the tree. + * + */ + remove() { + this.detach(); + let child = this.firstChild; + while (child) { + const next = child.next; + child.remove(); + child = next; + } + } + + reposition(newParent: Block | null, newPrev: Block | null) { + // Update the block's parent and siblings + this.parent = newParent; + this.prev = newPrev; + this.next = null; + + if (newParent) { + const prev = newPrev; + if (!prev) { + const next = newParent.firstChild; + newParent.firstChild = this; + if (next) { + this.next = next; + next.prev = this; + } + + } else { + // Update the next and prev pointers of the newPrev and next blocks + if (prev.next !== this) { + const next = prev.next; + if (next) { + next.prev = this + this.next = next; + } + prev.next = this; + } + } + + } + } + + // detach the block from its current position in the tree + detach() { + if (this.prev) { + this.prev.next = this.next; + } else if (this.parent) { + this.parent.firstChild = this.next; + } + if (this.next) { + this.next.prev = this.prev; + } + } + +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block_chain.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block_chain.ts new file mode 100644 index 0000000000..877f3592df --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/block_chain.ts @@ -0,0 +1,225 @@ +import { BlockData, BlockInterface, BlockType } from '$app/interfaces/index'; +import { set } from '../../utils/tool'; +import { Block } from './block'; +export interface BlockChangeProps { + block?: Block, + startBlock?: Block, + endBlock?: Block, + oldParentId?: string, + oldPrevId?: string +} +export class BlockChain { + private map: Map> = new Map(); + public head: Block | null = null; + + constructor(private onBlockChange: (command: string, data: BlockChangeProps) => void) { + + } + /** + * generate blocks from doc data + * @param id doc id + * @param map doc data + */ + rebuild = (id: string, map: Record>) => { + this.map.clear(); + this.head = this.createBlock(id, map[id].type, map[id].data); + + const callback = (block: Block) => { + const firstChildId = map[block.id].firstChild; + const nextId = map[block.id].next; + if (!block.firstChild && firstChildId) { + block.firstChild = this.createBlock(firstChildId, map[firstChildId].type, map[firstChildId].data); + block.firstChild.parent = block; + block.firstChild.prev = null; + } + if (!block.next && nextId) { + block.next = this.createBlock(nextId, map[nextId].type, map[nextId].data); + block.next.parent = block.parent; + block.next.prev = block; + } + } + this.traverse(callback); + } + + /** + * Traversing the block list from front to back + * @param callback It will be call when the block visited + * @param block block item, it will be equal head node when the block item is undefined + */ + traverse(callback: (_block: Block) => void, block?: Block) { + let currentBlock: Block | null = block || this.head; + while (currentBlock) { + callback(currentBlock); + if (currentBlock.firstChild) { + this.traverse(callback, currentBlock.firstChild); + } + currentBlock = currentBlock.next; + } + } + + /** + * get block data + * @param blockId string + * @returns Block + */ + getBlock = (blockId: string) => { + return this.map.get(blockId) || null; + } + + destroy() { + this.map.clear(); + this.head = null; + this.onBlockChange = () => null; + } + + /** + * Adds a new child block to the beginning of the current block's children list. + * + * @param {string} parentId + * @param {Object} content - The content of the new block, including its type and data. + * @param {string} content.type - The type of the new block. + * @param {Object} content.data - The data associated with the new block. + * @returns {Block} The newly created child block. + */ + prependChild(blockId: string, content: { type: BlockType, data: BlockData }): Block | null { + const parent = this.getBlock(blockId); + if (!parent) return null; + const newBlock = parent.prependChild(content); + + if (newBlock) { + this.map.set(newBlock?.id, newBlock); + this.onBlockChange('insert', { block: newBlock }); + } + + return newBlock; + } + + /** + * Add a new sibling block after this block. + * @param {string} blockId + * @param content The type and data for the new sibling block. + * @returns The newly created sibling block. + */ + addSibling(blockId: string, content: { type: BlockType, data: BlockData }): Block | null { + const block = this.getBlock(blockId); + if (!block) return null; + const newBlock = block.addSibling(content); + if (newBlock) { + this.map.set(newBlock?.id, newBlock); + this.onBlockChange('insert', { block: newBlock }); + } + return newBlock; + } + + /** + * Remove this block and its descendants from the tree. + * @param {string} blockId + */ + remove(blockId: string) { + const block = this.getBlock(blockId); + if (!block) return; + block.remove(); + this.map.delete(block.id); + this.onBlockChange('delete', { block }); + return block; + } + + /** + * Move this block to a new position in the tree. + * @param {string} blockId + * @param newParentId The new parent block of this block. If null, the block becomes a top-level block. + * @param newPrevId The new previous sibling block of this block. If null, the block becomes the first child of the new parent. + * @returns This block after it has been moved. + */ + move(blockId: string, newParentId: string, newPrevId: string): Block | null { + const block = this.getBlock(blockId); + if (!block) return null; + const oldParentId = block.parent?.id; + const oldPrevId = block.prev?.id; + block.detach(); + const newParent = this.getBlock(newParentId); + const newPrev = this.getBlock(newPrevId); + block.reposition(newParent, newPrev); + this.onBlockChange('move', { + block, + oldParentId, + oldPrevId + }); + return block; + } + + updateBlock(id: string, data: { path: string[], value: any }) { + const block = this.getBlock(id); + if (!block) return null; + + set(block, data.path, data.value); + this.onBlockChange('update', { + block + }); + return block; + } + + + moveBulk(startBlockId: string, endBlockId: string, newParentId: string, newPrevId: string): [Block, Block] | null { + const startBlock = this.getBlock(startBlockId); + const endBlock = this.getBlock(endBlockId); + if (!startBlock || !endBlock) return null; + + if (startBlockId === endBlockId) { + const block = this.move(startBlockId, newParentId, ''); + if (!block) return null; + return [block, block]; + } + + const oldParent = startBlock.parent; + const prev = startBlock.prev; + const newParent = this.getBlock(newParentId); + if (!oldParent || !newParent) return null; + + if (oldParent.firstChild === startBlock) { + oldParent.firstChild = endBlock.next; + } else if (prev) { + prev.next = endBlock.next; + } + startBlock.prev = null; + endBlock.next = null; + + startBlock.parent = newParent; + endBlock.parent = newParent; + const newPrev = this.getBlock(newPrevId); + if (!newPrev) { + const firstChild = newParent.firstChild; + newParent.firstChild = startBlock; + if (firstChild) { + endBlock.next = firstChild; + firstChild.prev = endBlock; + } + } else { + const next = newPrev.next; + newPrev.next = startBlock; + endBlock.next = next; + if (next) { + next.prev = endBlock; + } + } + + this.onBlockChange('move', { + startBlock, + endBlock, + oldParentId: oldParent.id, + oldPrevId: prev?.id + }); + + return [ + startBlock, + endBlock + ]; + } + + + private createBlock(id: string, type: BlockType, data: BlockData) { + const block = new Block(id, type, data); + this.map.set(id, block); + return block; + } +} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/op_adapter.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/op_adapter.ts new file mode 100644 index 0000000000..0c5c0b3190 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/op_adapter.ts @@ -0,0 +1,16 @@ +import { BackendOp, LocalOp } from "$app/interfaces"; + +export class OpAdapter { + + toBackendOp(localOp: LocalOp): BackendOp { + const backendOp: BackendOp = { ...localOp }; + // switch localOp type and generate backendOp + return backendOp; + } + + toLocalOp(backendOp: BackendOp): LocalOp { + const localOp: LocalOp = { ...backendOp }; + // switch backendOp type and generate localOp + return localOp; + } +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/operation.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/operation.ts new file mode 100644 index 0000000000..38f3a3fb76 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/operation.ts @@ -0,0 +1,153 @@ +import { BlockChain } from './block_chain'; +import { BlockInterface, BlockType, InsertOpData, LocalOp, UpdateOpData, moveOpData, moveRangeOpData, removeOpData, BlockData } from '$app/interfaces'; +import { BlockEditorSync } from './sync'; +import { Block } from './block'; + +export class Operation { + private sync: BlockEditorSync; + constructor(private blockChain: BlockChain) { + this.sync = new BlockEditorSync(); + } + + + splitNode( + retainId: string, + retainData: { path: string[], value: any }, + newBlockData: { + type: BlockType; + data: BlockData + }) { + const ops: { + type: LocalOp['type']; + data: LocalOp['data']; + }[] = []; + const newBlock = this.blockChain.addSibling(retainId, newBlockData); + const parentId = newBlock?.parent?.id; + const retainBlock = this.blockChain.getBlock(retainId); + if (!newBlock || !parentId || !retainBlock) return null; + + const insertOp = this.getInsertNodeOp({ + id: newBlock.id, + next: newBlock.next?.id || null, + firstChild: newBlock.firstChild?.id || null, + data: newBlock.data, + type: newBlock.type, + }, parentId, retainId); + + const updateOp = this.getUpdateNodeOp(retainId, retainData.path, retainData.value); + this.blockChain.updateBlock(retainId, retainData); + + ops.push(insertOp, updateOp); + const startBlock = retainBlock.firstChild; + if (startBlock) { + const startBlockId = startBlock.id; + let next: Block | null = startBlock.next; + let endBlockId = startBlockId; + while (next) { + endBlockId = next.id; + next = next.next; + } + + const moveOp = this.getMoveRangeOp([startBlockId, endBlockId], newBlock.id); + this.blockChain.moveBulk(startBlockId, endBlockId, newBlock.id, ''); + ops.push(moveOp); + } + + this.sync.sendOps(ops); + + return newBlock; + } + + updateNode(blockId: string, path: string[], value: T) { + const op = this.getUpdateNodeOp(blockId, path, value); + this.blockChain.updateBlock(blockId, { + path, + value + }); + this.sync.sendOps([op]); + } + private getUpdateNodeOp(blockId: string, path: string[], value: T): { + type: 'update', + data: UpdateOpData + } { + return { + type: 'update', + data: { + blockId, + path: path, + value + } + }; + } + + private getInsertNodeOp(block: T, parentId: string, prevId?: string): { + type: 'insert'; + data: InsertOpData + } { + return { + type: 'insert', + data: { + block, + parentId, + prevId + } + } + } + + private getMoveRangeOp(range: [string, string], newParentId: string, newPrevId?: string): { + type: 'move_range', + data: moveRangeOpData + } { + return { + type: 'move_range', + data: { + range, + newParentId, + newPrevId, + } + } + } + + private getMoveOp(blockId: string, newParentId: string, newPrevId?: string): { + type: 'move', + data: moveOpData + } { + return { + type: 'move', + data: { + blockId, + newParentId, + newPrevId + } + } + } + + private getRemoveOp(blockId: string): { + type: 'remove' + data: removeOpData + } { + return { + type: 'remove', + data: { + blockId + } + } + } + + applyOperation(op: LocalOp) { + switch (op.type) { + case 'insert': + + break; + + default: + break; + } + } + + destroy() { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + this.blockChain = null; + } +} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/sync.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/sync.ts new file mode 100644 index 0000000000..24070c0cd5 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/block_editor/core/sync.ts @@ -0,0 +1,48 @@ +import { BackendOp, LocalOp } from '$app/interfaces'; +import { OpAdapter } from './op_adapter'; + +/** + * BlockEditorSync is a class that synchronizes changes made to a block chain with a server. + * It allows for adding, removing, and moving blocks in the chain, and sends pending operations to the server. + */ +export class BlockEditorSync { + private version = 0; + private opAdapter: OpAdapter; + private pendingOps: BackendOp[] = []; + private appliedOps: LocalOp[] = []; + + constructor() { + this.opAdapter = new OpAdapter(); + } + + private applyOp(op: BackendOp): void { + const localOp = this.opAdapter.toLocalOp(op); + this.appliedOps.push(localOp); + } + + private receiveOps(ops: BackendOp[]): void { + // Apply the incoming operations to the local document + ops.sort((a, b) => a.version - b.version); + for (const op of ops) { + this.applyOp(op); + } + } + + private resolveConflict(): void { + // Implement conflict resolution logic here + } + + public sendOps(ops: { + type: LocalOp["type"]; + data: LocalOp["data"] + }[]) { + const backendOps = ops.map(op => this.opAdapter.toBackendOp({ + ...op, + version: this.version + })); + this.pendingOps.push(...backendOps); + // Send the pending operations to the server + console.log('==== sync pending ops ====', [...this.pendingOps]); + } + +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/index.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/index.ts index 01f49f656b..658b284906 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/index.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/block_editor/index.ts @@ -1,48 +1,60 @@ +// Import dependencies import { BlockInterface } from '../interfaces'; -import { BlockDataManager } from './block'; -import { TreeManager } from './tree'; +import { BlockChain, BlockChangeProps } from './core/block_chain'; +import { RenderTree } from './view/tree'; +import { Operation } from './core/operation'; /** - * BlockEditor is a document data manager that operates on and renders data through managing blockData and RenderTreeManager. - * The render tree will be re-render and update react component when block makes changes to the data. - * RectManager updates the cache of node rect when the react component update is completed. + * The BlockEditor class manages a block chain and a render tree for a document editor. + * The block chain stores the content blocks of the document in sequence, while the + * render tree displays the document as a hierarchical tree structure. */ export class BlockEditor { - // blockData manages document block data, including operations such as add, delete, update, and move. - public blockData: BlockDataManager; - // RenderTreeManager manages data rendering, including the construction and updating of the render tree. - public renderTree: TreeManager; + // Public properties + public blockChain: BlockChain; // (local data) the block chain used to store the document + public renderTree: RenderTree; // the render tree used to display the document + public operation: Operation; + /** + * Constructs a new BlockEditor object. + * @param id - the ID of the document + * @param data - the initial data for the document + */ + constructor(private id: string, data: Record) { + // Create the block chain and render tree + this.blockChain = new BlockChain(this.blockChange); + this.operation = new Operation(this.blockChain); + this.changeDoc(id, data); - constructor(private id: string, data: Record) { - this.blockData = new BlockDataManager(id, data); - this.renderTree = new TreeManager(this.blockData.getBlock); + this.renderTree = new RenderTree(this.blockChain); } /** - * update id and map when the doc is change - * @param id - * @param data + * Updates the document ID and block chain when the document changes. + * @param id - the new ID of the document + * @param data - the updated data for the document */ changeDoc = (id: string, data: Record) => { - console.log('==== change document ====', id, data) + console.log('==== change document ====', id, data); + + // Update the document ID and rebuild the block chain this.id = id; - this.blockData.setBlocksMap(id, data); + this.blockChain.rebuild(id, data); } + + /** + * Destroys the block chain and render tree. + */ destroy = () => { + // Destroy the block chain and render tree + this.blockChain.destroy(); this.renderTree.destroy(); - this.blockData.destroy(); + this.operation.destroy(); } - + + private blockChange = (command: string, data: BlockChangeProps) => { + this.renderTree.onBlockChange(command, data); + } + } -let blockEditorInstance: BlockEditor | null; - -export function getBlockEditor() { - return blockEditorInstance; -} - -export function createBlockEditor(id: string, data: Record) { - blockEditorInstance = new BlockEditor(id, data); - return blockEditorInstance; -} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/rect.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/rect.ts deleted file mode 100644 index 5398ab4a6f..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/rect.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { TreeNodeInterface } from "../interfaces"; - - -export function calculateBlockRect(blockId: string) { - const el = document.querySelectorAll(`[data-block-id=${blockId}]`)[0]; - return el?.getBoundingClientRect(); -} - -export class RectManager { - map: Map; - - orderList: Set; - - private updatedQueue: Set; - - constructor(private getTreeNode: (nodeId: string) => TreeNodeInterface | null) { - this.map = new Map(); - this.orderList = new Set(); - this.updatedQueue = new Set(); - } - - build() { - console.log('====update all blocks position====') - this.orderList.forEach(id => this.updateNodeRect(id)); - } - - getNodeRect = (nodeId: string) => { - return this.map.get(nodeId) || null; - } - - update() { - // In order to avoid excessive calculation frequency - // calculate and update the block position information in the queue every frame - requestAnimationFrame(() => { - // there is nothing to do if the updated queue is empty - if (this.updatedQueue.size === 0) return; - console.log(`==== update ${this.updatedQueue.size} blocks rect cache ====`) - this.updatedQueue.forEach((id: string) => { - const rect = calculateBlockRect(id); - this.map.set(id, rect); - this.updatedQueue.delete(id); - }); - }); - } - - updateNodeRect = (nodeId: string) => { - if (this.updatedQueue.has(nodeId)) return; - let node: TreeNodeInterface | null = this.getTreeNode(nodeId); - - // When one of the blocks is updated - // the positions of all its parent and child blocks need to be updated - while(node) { - node.parent?.children.forEach(child => this.updatedQueue.add(child.id)); - node = node.parent; - } - - this.update(); - } - - destroy() { - this.map.clear(); - this.orderList.clear(); - this.updatedQueue.clear(); - } - -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/tree.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/tree.ts deleted file mode 100644 index bc545139fc..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/block_editor/tree.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { RectManager } from "./rect"; -import { BlockInterface, BlockData, BlockType, TreeNodeInterface } from '../interfaces/index'; - -export class TreeManager { - - // RenderTreeManager holds RectManager, which manages the position information of each node in the render tree. - private rect: RectManager; - - root: TreeNode | null = null; - - map: Map = new Map(); - - constructor(private getBlock: (blockId: string) => BlockInterface | null) { - this.rect = new RectManager(this.getTreeNode); - } - - /** - * Get render node data by nodeId - * @param nodeId string - * @returns TreeNode - */ - getTreeNode = (nodeId: string): TreeNodeInterface | null => { - return this.map.get(nodeId) || null; - } - - /** - * build tree node for rendering - * @param rootId - * @returns - */ - build(rootId: string): TreeNode | null { - const head = this.getBlock(rootId); - - if (!head) return null; - - this.root = new TreeNode(head); - - let node = this.root; - - // loop line - while (node) { - this.map.set(node.id, node); - this.rect.orderList.add(node.id); - - const block = this.getBlock(node.id)!; - const next = block.next ? this.getBlock(block.next) : null; - const firstChild = block.firstChild ? this.getBlock(block.firstChild) : null; - - // find next line - if (firstChild) { - // the next line is node's first child - const child = new TreeNode(firstChild); - node.addChild(child); - node = child; - } else if (next) { - // the next line is node's sibling - const sibling = new TreeNode(next); - node.parent?.addChild(sibling); - node = sibling; - } else { - // the next line is parent's sibling - let isFind = false; - while(node.parent) { - const parentId = node.parent.id; - const parent = this.getBlock(parentId)!; - const parentNext = parent.next ? this.getBlock(parent.next) : null; - if (parentNext) { - const parentSibling = new TreeNode(parentNext); - node.parent?.parent?.addChild(parentSibling); - node = parentSibling; - isFind = true; - break; - } else { - node = node.parent; - } - } - - if (!isFind) { - // Exit if next line not found - break; - } - - } - } - - return this.root; - } - - /** - * update dom rects cache - */ - updateRects = () => { - this.rect.build(); - } - - /** - * get block rect cache - * @param id string - * @returns DOMRect - */ - getNodeRect = (nodeId: string) => { - return this.rect.getNodeRect(nodeId); - } - - /** - * update block rect cache - * @param id string - */ - updateNodeRect = (nodeId: string) => { - this.rect.updateNodeRect(nodeId); - } - - destroy() { - this.rect?.destroy(); - } -} - - -class TreeNode implements TreeNodeInterface { - id: string; - type: BlockType; - parent: TreeNode | null = null; - children: TreeNode[] = []; - data: BlockData; - - constructor({ - id, - type, - data - }: BlockInterface) { - this.id = id; - this.data = data; - this.type = type; - } - - addChild(node: TreeNode) { - node.parent = this; - this.children.push(node); - } -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/block_position.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/block_position.ts new file mode 100644 index 0000000000..a2841d8a3b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/block_position.ts @@ -0,0 +1,73 @@ +import { RegionGrid, BlockPosition } from './region_grid'; +export class BlockPositionManager { + private regionGrid: RegionGrid; + private viewportBlocks: Set = new Set(); + private blockPositions: Map = new Map(); + private observer: IntersectionObserver; + private container: HTMLDivElement | null = null; + + constructor(container: HTMLDivElement) { + this.container = container; + this.regionGrid = new RegionGrid(container.offsetHeight); + this.observer = new IntersectionObserver((entries) => { + for (const entry of entries) { + const blockId = entry.target.getAttribute('data-block-id'); + if (!blockId) return; + if (entry.isIntersecting) { + this.updateBlockPosition(blockId); + this.viewportBlocks.add(blockId); + } else { + this.viewportBlocks.delete(blockId); + } + } + }, { root: container }); + } + + observeBlock(node: HTMLDivElement) { + this.observer.observe(node); + return { + unobserve: () => this.observer.unobserve(node), + } + } + + getBlockPosition(blockId: string) { + if (!this.blockPositions.has(blockId)) { + this.updateBlockPosition(blockId); + } + return this.blockPositions.get(blockId); + } + + updateBlockPosition(blockId: string) { + if (!this.container) return; + const node = document.querySelector(`[data-block-id=${blockId}]`) as HTMLDivElement; + if (!node) return; + const rect = node.getBoundingClientRect(); + const position = { + id: blockId, + x: rect.x, + y: rect.y + this.container.scrollTop, + height: rect.height, + width: rect.width + }; + const prevPosition = this.blockPositions.get(blockId); + if (prevPosition && prevPosition.x === position.x && + prevPosition.y === position.y && + prevPosition.height === position.height && + prevPosition.width === position.width) { + return; + } + this.blockPositions.set(blockId, position); + this.regionGrid.removeBlock(blockId); + this.regionGrid.addBlock(position); + } + + getIntersectBlocks(startX: number, startY: number, endX: number, endY: number): BlockPosition[] { + return this.regionGrid.getIntersectBlocks(startX, startY, endX, endY); + } + + destroy() { + this.container = null; + this.observer.disconnect(); + } + +} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/region_grid.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/region_grid.ts new file mode 100644 index 0000000000..5f06f253ad --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/region_grid.ts @@ -0,0 +1,81 @@ +export interface BlockPosition { + id: string; + x: number; + y: number; + height: number; + width: number; +} +interface BlockRegion { + regionX: number; + regionY: number; + blocks: BlockPosition[]; +} + +export class RegionGrid { + private regions: BlockRegion[][]; + private regionSize: number; + + constructor(regionSize: number) { + this.regionSize = regionSize; + this.regions = []; + } + + addBlock(blockPosition: BlockPosition) { + const regionX = Math.floor(blockPosition.x / this.regionSize); + const regionY = Math.floor(blockPosition.y / this.regionSize); + + let region = this.regions[regionY]?.[regionX]; + if (!region) { + region = { + regionX, + regionY, + blocks: [] + }; + if (!this.regions[regionY]) { + this.regions[regionY] = []; + } + this.regions[regionY][regionX] = region; + } + + region.blocks.push(blockPosition); + } + + removeBlock(blockId: string) { + for (const rows of this.regions) { + for (const region of rows) { + if (!region) return; + const blockIndex = region.blocks.findIndex(b => b.id === blockId); + if (blockIndex !== -1) { + region.blocks.splice(blockIndex, 1); + return; + } + } + } + } + + + getIntersectBlocks(startX: number, startY: number, endX: number, endY: number): BlockPosition[] { + const selectedBlocks: BlockPosition[] = []; + + const startRegionX = Math.floor(startX / this.regionSize); + const startRegionY = Math.floor(startY / this.regionSize); + const endRegionX = Math.floor(endX / this.regionSize); + const endRegionY = Math.floor(endY / this.regionSize); + + for (let y = startRegionY; y <= endRegionY; y++) { + for (let x = startRegionX; x <= endRegionX; x++) { + const region = this.regions[y]?.[x]; + if (region) { + for (const block of region.blocks) { + if (block.x + block.width - 1 >= startX && block.x <= endX && + block.y + block.height - 1 >= startY && block.y <= endY) { + selectedBlocks.push(block); + } + } + } + } + } + + return selectedBlocks; + } +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree.ts new file mode 100644 index 0000000000..4eb136ff09 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree.ts @@ -0,0 +1,165 @@ +import { BlockChain, BlockChangeProps } from '../core/block_chain'; +import { Block } from '../core/block'; +import { TreeNode } from "./tree_node"; +import { BlockPositionManager } from './block_position'; +import { filterSelections } from '@/appflowy_app/utils/block_selection'; + +export class RenderTree { + public blockPositionManager?: BlockPositionManager; + + private map: Map = new Map(); + private root: TreeNode | null = null; + private selections: Set = new Set(); + constructor(private blockChain: BlockChain) { + } + + + createPositionManager(container: HTMLDivElement) { + this.blockPositionManager = new BlockPositionManager(container); + } + + observeBlock(node: HTMLDivElement) { + return this.blockPositionManager?.observeBlock(node); + } + + getBlockPosition(nodeId: string) { + return this.blockPositionManager?.getBlockPosition(nodeId) || null; + } + /** + * Get the TreeNode data by nodeId + * @param nodeId string + * @returns TreeNode|null + */ + getTreeNode = (nodeId: string): TreeNode | null => { + // Return the TreeNode instance from the map or null if it does not exist + return this.map.get(nodeId) || null; + } + + private createNode(block: Block): TreeNode { + if (this.map.has(block.id)) { + return this.map.get(block.id)!; + } + const node = new TreeNode(block); + this.map.set(block.id, node); + return node; + } + + + buildDeep(rootId: string): TreeNode | null { + this.map.clear(); + // Define a callback function for the blockChain.traverse() method + const callback = (block: Block) => { + // Check if the TreeNode instance already exists in the map + const node = this.createNode(block); + + // Add the TreeNode instance to the map + this.map.set(block.id, node); + + // Add the first child of the block as a child of the current TreeNode instance + const firstChild = block.firstChild; + if (firstChild) { + const child = this.createNode(firstChild); + node.addChild(child); + this.map.set(child.id, child); + } + + // Add the next block as a sibling of the current TreeNode instance + const next = block.next; + if (next) { + const nextNode = this.createNode(next); + node.parent?.addChild(nextNode); + this.map.set(next.id, nextNode); + } + } + + // Traverse the blockChain using the callback function + this.blockChain.traverse(callback); + + // Get the root node from the map and return it + const root = this.map.get(rootId)!; + this.root = root; + return root || null; + } + + + forceUpdate(nodeId: string, shouldUpdateChildren = false) { + const block = this.blockChain.getBlock(nodeId); + if (!block) return null; + const node = this.createNode(block); + if (!node) return null; + + if (shouldUpdateChildren) { + const children: TreeNode[] = []; + let childBlock = block.firstChild; + + while(childBlock) { + const child = this.createNode(childBlock); + child.update(childBlock, child.children); + children.push(child); + childBlock = childBlock.next; + } + + node.update(block, children); + node?.reRender(); + node?.children.forEach(child => { + child.reRender(); + }) + } else { + node.update(block, node.children); + node?.reRender(); + } + } + + onBlockChange(command: string, data: BlockChangeProps) { + const { block, startBlock, endBlock, oldParentId = '', oldPrevId = '' } = data; + switch (command) { + case 'insert': + if (block?.parent) this.forceUpdate(block.parent.id, true); + break; + case 'update': + this.forceUpdate(block!.id); + break; + case 'move': + if (oldParentId) this.forceUpdate(oldParentId, true); + if (block?.parent) this.forceUpdate(block.parent.id, true); + if (startBlock?.parent) this.forceUpdate(startBlock.parent.id, true); + break; + default: + break; + } + + } + + updateSelections(selections: string[]) { + const newSelections = filterSelections(selections, this.map); + + let isDiff = false; + if (newSelections.length !== this.selections.size) { + isDiff = true; + } + + const selectedBlocksSet = new Set(newSelections); + if (Array.from(this.selections).some((id) => !selectedBlocksSet.has(id))) { + isDiff = true; + } + + if (isDiff) { + const shouldUpdateIds = new Set([...this.selections, ...newSelections]); + this.selections = selectedBlocksSet; + shouldUpdateIds.forEach((id) => this.forceUpdate(id)); + } + } + + isSelected(nodeId: string) { + return this.selections.has(nodeId); + } + + /** + * Destroy the RenderTreeRectManager instance + */ + destroy() { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + this.blockChain = null; + } +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree_node.ts b/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree_node.ts new file mode 100644 index 0000000000..9ed78bd4b4 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/block_editor/view/tree_node.ts @@ -0,0 +1,59 @@ +import { BlockData, BlockType } from '$app/interfaces/index'; +import { Block } from '../core/block'; + +/** + * Represents a node in a tree structure of blocks. + */ +export class TreeNode { + id: string; + type: BlockType; + parent: TreeNode | null = null; + children: TreeNode[] = []; + data: BlockData; + + private forceUpdate?: () => void; + + /** + * Create a new TreeNode instance. + * @param block - The block data used to create the node. + */ + constructor(private _block: Block) { + this.id = _block.id; + this.data = _block.data; + this.type = _block.type; + } + + registerUpdate(forceUpdate: () => void) { + this.forceUpdate = forceUpdate; + } + + unregisterUpdate() { + this.forceUpdate = undefined; + } + + reRender() { + this.forceUpdate?.(); + } + + update(block: Block, children: TreeNode[]) { + this.data = block.data; + this.children = []; + children.forEach(child => { + this.addChild(child); + }) + } + + /** + * Add a child node to the current node. + * @param node - The child node to add. + */ + addChild(node: TreeNode) { + node.parent = this; + this.children.push(node); + } + + get block() { + return this._block; + } + +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/FormatButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/FormatButton.tsx index cbe27de694..1409680f24 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/FormatButton.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/FormatButton.tsx @@ -1,31 +1,12 @@ -import { useSlate } from 'slate-react'; import { toggleFormat, isFormatActive } from '@/appflowy_app/utils/slate/format'; import IconButton from '@mui/material/IconButton'; import Tooltip from '@mui/material/Tooltip'; -import { useMemo } from 'react'; -import { FormatBold, FormatUnderlined, FormatItalic, CodeOutlined, StrikethroughSOutlined } from '@mui/icons-material'; -import { command, iconSize } from '$app/constants/toolbar'; -const FormatButton = ({ format, icon }: { format: string; icon: string }) => { - const editor = useSlate(); - - const renderComponent = useMemo(() => { - switch (icon) { - case 'bold': - return ; - case 'underlined': - return ; - case 'italic': - return ; - case 'code': - return ; - case 'strikethrough': - return ; - default: - break; - } - }, [icon]); +import { command } from '$app/constants/toolbar'; +import FormatIcon from './FormatIcon'; +import { BaseEditor } from 'slate'; +const FormatButton = ({ editor, format, icon }: { editor: BaseEditor; format: string; icon: string }) => { return ( { sx={{ color: isFormatActive(editor, format) ? '#00BCF0' : 'white' }} onClick={() => toggleFormat(editor, format)} > - {renderComponent} + ); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/FormatIcon.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/FormatIcon.tsx new file mode 100644 index 0000000000..371ec6585c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/FormatIcon.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { FormatBold, FormatUnderlined, FormatItalic, CodeOutlined, StrikethroughSOutlined } from '@mui/icons-material'; +import { iconSize } from '$app/constants/toolbar'; + +export default function FormatIcon({ icon }: { icon: string }) { + switch (icon) { + case 'bold': + return ; + case 'underlined': + return ; + case 'italic': + return ; + case 'code': + return ; + case 'strikethrough': + return ; + default: + return null; + } +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/components.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/components.tsx deleted file mode 100644 index 0a18c3f5e9..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/components.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import ReactDOM from 'react-dom'; -export const Portal = ({ blockId, children }: { blockId: string; children: JSX.Element }) => { - const root = document.querySelectorAll(`[data-block-id=${blockId}]`)[0]; - return typeof document === 'object' && root ? ReactDOM.createPortal(children, root) : null; -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.hooks.ts new file mode 100644 index 0000000000..8319291046 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.hooks.ts @@ -0,0 +1,36 @@ +import { useEffect, useRef } from 'react'; +import { useFocused, useSlate } from 'slate-react'; +import { calcToolbarPosition } from '@/appflowy_app/utils/slate/toolbar'; +import { TreeNode } from '$app/block_editor/view/tree_node'; + +export function useHoveringToolbar({node}: { + node: TreeNode +}) { + const editor = useSlate(); + const inFocus = useFocused(); + const ref = useRef(null); + + useEffect(() => { + const el = ref.current; + if (!el) return; + const nodeRect = document.querySelector(`[data-block-id=${node.id}]`)?.getBoundingClientRect(); + + if (!nodeRect) return; + const position = calcToolbarPosition(editor, el, nodeRect); + + if (!position) { + el.style.opacity = '0'; + el.style.zIndex = '-1'; + } else { + el.style.opacity = '1'; + el.style.zIndex = '1'; + el.style.top = position.top; + el.style.left = position.left; + } + }); + return { + ref, + inFocus, + editor + } +} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.tsx index 7b8454800b..dcd502905f 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.tsx @@ -1,29 +1,10 @@ -import { useEffect, useRef } from 'react'; -import { useFocused, useSlate } from 'slate-react'; import FormatButton from './FormatButton'; import Portal from './Portal'; -import { calcToolbarPosition } from '@/appflowy_app/utils/slate/toolbar'; - -const HoveringToolbar = ({ blockId }: { blockId: string }) => { - const editor = useSlate(); - const inFocus = useFocused(); - const ref = useRef(null); - - useEffect(() => { - const el = ref.current; - if (!el) return; - - const position = calcToolbarPosition(editor, el, blockId); - - if (!position) { - el.style.opacity = '0'; - } else { - el.style.opacity = '1'; - el.style.top = position.top; - el.style.left = position.left; - } - }); +import { TreeNode } from '$app/block_editor/view/tree_node'; +import { useHoveringToolbar } from './index.hooks'; +const HoveringToolbar = ({ blockId, node }: { blockId: string; node: TreeNode }) => { + const { inFocus, ref, editor } = useHoveringToolbar({ node }); if (!inFocus) return null; return ( @@ -40,7 +21,7 @@ const HoveringToolbar = ({ blockId }: { blockId: string }) => { }} > {['bold', 'italic', 'underlined', 'strikethrough', 'code'].map((format) => ( - + ))} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/BlockComponet.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/BlockComponet.hooks.ts new file mode 100644 index 0000000000..20e31a1793 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/BlockComponet.hooks.ts @@ -0,0 +1,36 @@ +import { useEffect, useState, useRef, useContext } from 'react'; + +import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node'; +import { BlockContext } from '$app/utils/block'; + +export function useBlockComponent({ + node +}: { + node: TreeNode +}) { + const { blockEditor } = useContext(BlockContext); + + const [version, forceUpdate] = useState(0); + const myRef = useRef(null); + + const isSelected = blockEditor?.renderTree.isSelected(node.id); + + useEffect(() => { + if (!myRef.current) { + return; + } + const observe = blockEditor?.renderTree.observeBlock(myRef.current); + node.registerUpdate(() => forceUpdate((prev) => prev + 1)); + + return () => { + node.unregisterUpdate(); + observe?.unobserve(); + }; + }, []); + return { + version, + myRef, + isSelected, + className: `relative my-[1px] px-1` + } +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/index.tsx new file mode 100644 index 0000000000..9c8ee223dd --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockComponent/index.tsx @@ -0,0 +1,91 @@ +import React, { forwardRef } from 'react'; +import { BlockCommonProps, BlockType } from '$app/interfaces'; +import PageBlock from '../PageBlock'; +import TextBlock from '../TextBlock'; +import HeadingBlock from '../HeadingBlock'; +import ListBlock from '../ListBlock'; +import CodeBlock from '../CodeBlock'; +import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node'; +import { withErrorBoundary } from 'react-error-boundary'; +import { ErrorBoundaryFallbackComponent } from '../BlockList/BlockList.hooks'; +import { useBlockComponent } from './BlockComponet.hooks'; + +const BlockComponent = forwardRef( + ( + { + node, + renderChild, + ...props + }: { node: TreeNode; renderChild?: (_node: TreeNode) => React.ReactNode } & React.DetailedHTMLProps< + React.HTMLAttributes, + HTMLDivElement + >, + ref: React.ForwardedRef + ) => { + const { myRef, className, version, isSelected } = useBlockComponent({ + node, + }); + + const renderComponent = () => { + let BlockComponentClass: (_: BlockCommonProps) => JSX.Element | null; + switch (node.type) { + case BlockType.PageBlock: + BlockComponentClass = PageBlock; + break; + case BlockType.TextBlock: + BlockComponentClass = TextBlock; + break; + case BlockType.HeadingBlock: + BlockComponentClass = HeadingBlock; + break; + case BlockType.ListBlock: + BlockComponentClass = ListBlock; + break; + case BlockType.CodeBlock: + BlockComponentClass = CodeBlock; + break; + default: + break; + } + + const blockProps: BlockCommonProps = { + version, + node, + }; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + if (BlockComponentClass) { + return ; + } + return null; + }; + + return ( +
{ + myRef.current = el; + if (typeof ref === 'function') { + ref(el); + } else if (ref) { + ref.current = el; + } + }} + {...props} + data-block-id={node.id} + data-block-selected={isSelected} + className={props.className ? `${props.className} ${className}` : className} + > + {renderComponent()} + {renderChild ? node.children.map(renderChild) : null} +
+ {isSelected ?
: null} +
+ ); + } +); + +const ComponentWithErrorBoundary = withErrorBoundary(BlockComponent, { + FallbackComponent: ErrorBoundaryFallbackComponent, +}); +export default React.memo(ComponentWithErrorBoundary); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockComponent.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockComponent.tsx deleted file mode 100644 index 051081ebaf..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockComponent.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; -import { BlockType, TreeNodeInterface } from '$app/interfaces'; -import PageBlock from '../PageBlock'; -import TextBlock from '../TextBlock'; -import HeadingBlock from '../HeadingBlock'; -import ListBlock from '../ListBlock'; -import CodeBlock from '../CodeBlock'; - -function BlockComponent({ - node, - ...props -}: { node: TreeNodeInterface } & React.DetailedHTMLProps, HTMLDivElement>) { - const renderComponent = () => { - switch (node.type) { - case BlockType.PageBlock: - return ; - case BlockType.TextBlock: - return ; - case BlockType.HeadingBlock: - return ; - case BlockType.ListBlock: - return ; - case BlockType.CodeBlock: - return ; - default: - return null; - } - }; - - return ( -
- {renderComponent()} - {props.children} -
-
- ); -} - -export default React.memo(BlockComponent); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockList.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockList.hooks.tsx new file mode 100644 index 0000000000..0d673a47e8 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockList.hooks.tsx @@ -0,0 +1,92 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { BlockEditor } from '@/appflowy_app/block_editor'; +import { TreeNode } from '$app/block_editor/view/tree_node'; +import { Alert } from '@mui/material'; +import { FallbackProps } from 'react-error-boundary'; +import { TextBlockManager } from '@/appflowy_app/block_editor/blocks/text_block'; +import { TextBlockContext } from '@/appflowy_app/utils/slate/context'; +import { useVirtualizer } from '@tanstack/react-virtual'; +export interface BlockListProps { + blockId: string; + blockEditor: BlockEditor; +} + +const defaultSize = 45; + +export function useBlockList({ blockId, blockEditor }: BlockListProps) { + const [root, setRoot] = useState(null); + + const parentRef = useRef(null); + + const rowVirtualizer = useVirtualizer({ + count: root?.children.length || 0, + getScrollElement: () => parentRef.current, + overscan: 5, + estimateSize: () => { + return defaultSize; + }, + }); + + const [version, forceUpdate] = useState(0); + + const buildDeepTree = useCallback(() => { + const treeNode = blockEditor.renderTree.buildDeep(blockId); + setRoot(treeNode); + }, [blockEditor]); + + useEffect(() => { + if (!parentRef.current) return; + blockEditor.renderTree.createPositionManager(parentRef.current); + buildDeepTree(); + + return () => { + blockEditor.destroy(); + }; + }, [blockId, blockEditor]); + + useEffect(() => { + root?.registerUpdate(() => forceUpdate((prev) => prev + 1)); + return () => { + root?.unregisterUpdate(); + }; + }, [root]); + + return { + root, + rowVirtualizer, + parentRef, + blockEditor, + }; +} + +export function ErrorBoundaryFallbackComponent({ error, resetErrorBoundary }: FallbackProps) { + return ( + +

Something went wrong:

+
{error.message}
+ +
+ ); +} + +export function withTextBlockManager(Component: (props: BlockListProps) => React.ReactElement) { + return (props: BlockListProps) => { + const textBlockManager = new TextBlockManager(props.blockEditor.operation); + + useEffect(() => { + return () => { + textBlockManager.destroy(); + }; + }, []); + + return ( + + + + ); + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockListTitle.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockListTitle.tsx new file mode 100644 index 0000000000..f74ae72283 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockListTitle.tsx @@ -0,0 +1,18 @@ +import TextBlock from '../TextBlock'; +import { TreeNode } from '$app/block_editor/view/tree_node'; + +export default function BlockListTitle({ node }: { node: TreeNode | null }) { + if (!node) return null; + return ( +
+ +
+ ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/ListFallbackComponent.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/ListFallbackComponent.tsx new file mode 100644 index 0000000000..6078180374 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/ListFallbackComponent.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import Typography, { TypographyProps } from '@mui/material/Typography'; +import Skeleton from '@mui/material/Skeleton'; +import Grid from '@mui/material/Grid'; + +const variants = ['h1', 'h3', 'body1', 'caption'] as readonly TypographyProps['variant'][]; + +export default function ListFallbackComponent() { + return ( +
+
+
+ + + +
+
+ + + {variants.map((variant) => ( + + + + ))} + + +
+
+
+ ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/index.tsx index 7badc069b1..9a8709ea64 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/index.tsx @@ -1,43 +1,58 @@ -import BlockComponent from './BlockComponent'; -import React, { useEffect } from 'react'; -import { debounce } from '@/appflowy_app/utils/tool'; -import { getBlockEditor } from '../../../block_editor'; +import React from 'react'; +import { BlockListProps, useBlockList, withTextBlockManager } from './BlockList.hooks'; +import { withErrorBoundary } from 'react-error-boundary'; +import ListFallbackComponent from './ListFallbackComponent'; +import BlockListTitle from './BlockListTitle'; +import BlockComponent from '../BlockComponent'; +import BlockSelection from '../BlockSelection'; -const RESIZE_DELAY = 200; - -function BlockList({ blockId }: { blockId: string }) { - const blockEditor = getBlockEditor(); - if (!blockEditor) return null; - - const root = blockEditor.renderTree.build(blockId); - console.log('==== build tree ====', root); - - useEffect(() => { - // update rect cache when did mount - blockEditor.renderTree.updateRects(); - - const resize = debounce(() => { - // update rect cache when window resized - blockEditor.renderTree.updateRects(); - }, RESIZE_DELAY); - - window.addEventListener('resize', resize); - - return () => { - window.removeEventListener('resize', resize); - }; - }, []); +function BlockList(props: BlockListProps) { + const { root, rowVirtualizer, parentRef, blockEditor } = useBlockList(props); + const virtualItems = rowVirtualizer.getVirtualItems(); return ( -
-
{root?.data.title}
-
- {root && root.children.length > 0 - ? root.children.map((node) => ) - : null} +
+
+
+ {root && virtualItems.length ? ( +
+ {virtualItems.map((virtualRow) => { + const id = root.children[virtualRow.index].id; + return ( +
+ {virtualRow.index === 0 ? : null} + +
+ ); + })} +
+ ) : null} +
+ {parentRef.current ? : null}
); } -export default React.memo(BlockList); +const ListWithErrorBoundary = withErrorBoundary(withTextBlockManager(BlockList), { + FallbackComponent: ListFallbackComponent, +}); + +export default React.memo(ListWithErrorBoundary); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/BlockSelection.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/BlockSelection.hooks.tsx new file mode 100644 index 0000000000..00bc05f2d1 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/BlockSelection.hooks.tsx @@ -0,0 +1,137 @@ +import { BlockEditor } from '@/appflowy_app/block_editor'; +import { useEffect, useRef, useState, useCallback, useMemo } from 'react'; + +export function useBlockSelection({ container, blockEditor }: { container: HTMLDivElement; blockEditor: BlockEditor }) { + const blockPositionManager = blockEditor.renderTree.blockPositionManager; + + const [isDragging, setDragging] = useState(false); + const pointRef = useRef([]); + const startScrollTopRef = useRef(0); + + const [rect, setRect] = useState<{ + startX: number; + startY: number; + endX: number; + endY: number; + } | null>(null); + + const style = useMemo(() => { + if (!rect) return; + const { startX, endX, startY, endY } = rect; + const x = Math.min(startX, endX); + const y = Math.min(startY, endY); + const width = Math.abs(endX - startX); + const height = Math.abs(endY - startY); + return { + left: x - container.scrollLeft + 'px', + top: y - container.scrollTop + 'px', + width: width + 'px', + height: height + 'px', + }; + }, [rect]); + + const isPointInBlock = useCallback((target: HTMLElement | null) => { + let node = target; + while (node) { + if (node.getAttribute('data-block-id')) { + return true; + } + node = node.parentElement; + } + return false; + }, []); + + const handleDragStart = useCallback((e: MouseEvent) => { + if (isPointInBlock(e.target as HTMLElement)) { + return; + } + e.preventDefault(); + setDragging(true); + + const startX = e.clientX + container.scrollLeft; + const startY = e.clientY + container.scrollTop; + pointRef.current = [startX, startY]; + startScrollTopRef.current = container.scrollTop; + setRect({ + startX, + startY, + endX: startX, + endY: startY, + }); + }, []); + + const calcIntersectBlocks = useCallback( + (clientX: number, clientY: number) => { + if (!isDragging || !blockPositionManager) return; + const [startX, startY] = pointRef.current; + const endX = clientX + container.scrollLeft; + const endY = clientY + container.scrollTop; + + setRect({ + startX, + startY, + endX, + endY, + }); + const selectedBlocks = blockPositionManager.getIntersectBlocks( + Math.min(startX, endX), + Math.min(startY, endY), + Math.max(startX, endX), + Math.max(startY, endY) + ); + const ids = selectedBlocks.map((item) => item.id); + blockEditor.renderTree.updateSelections(ids); + }, + [isDragging] + ); + + const handleDraging = useCallback( + (e: MouseEvent) => { + if (!isDragging || !blockPositionManager) return; + e.preventDefault(); + calcIntersectBlocks(e.clientX, e.clientY); + + const { top, bottom } = container.getBoundingClientRect(); + if (e.clientY >= bottom) { + const delta = e.clientY - bottom; + container.scrollBy(0, delta); + } else if (e.clientY <= top) { + const delta = e.clientY - top; + container.scrollBy(0, delta); + } + }, + [isDragging] + ); + + const handleDragEnd = useCallback( + (e: MouseEvent) => { + if (isPointInBlock(e.target as HTMLElement) && !isDragging) { + blockEditor.renderTree.updateSelections([]); + return; + } + if (!isDragging) return; + e.preventDefault(); + calcIntersectBlocks(e.clientX, e.clientY); + setDragging(false); + setRect(null); + }, + [isDragging] + ); + + useEffect(() => { + window.addEventListener('mousedown', handleDragStart); + window.addEventListener('mousemove', handleDraging); + window.addEventListener('mouseup', handleDragEnd); + + return () => { + window.removeEventListener('mousedown', handleDragStart); + window.removeEventListener('mousemove', handleDraging); + window.removeEventListener('mouseup', handleDragEnd); + }; + }, [handleDragStart, handleDragEnd, handleDraging]); + + return { + isDragging, + style, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/index.tsx new file mode 100644 index 0000000000..4ef554d489 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockSelection/index.tsx @@ -0,0 +1,18 @@ +import { useBlockSelection } from './BlockSelection.hooks'; +import { BlockEditor } from '$app/block_editor'; +import React from 'react'; + +function BlockSelection({ container, blockEditor }: { container: HTMLDivElement; blockEditor: BlockEditor }) { + const { isDragging, style } = useBlockSelection({ + container, + blockEditor, + }); + + return ( +
+ {isDragging ?
: null} +
+ ); +} + +export default React.memo(BlockSelection); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/CodeBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/CodeBlock/index.tsx index ab04d15820..eb34844d2c 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/CodeBlock/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/CodeBlock/index.tsx @@ -1,6 +1,6 @@ -import React from 'react'; -import { TreeNodeInterface } from '$app/interfaces'; +import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node'; +import { BlockCommonProps } from '@/appflowy_app/interfaces'; -export default function CodeBlock({ node }: { node: TreeNodeInterface }) { +export default function CodeBlock({ node }: BlockCommonProps) { return
{node.data.text}
; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/ColumnBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/ColumnBlock/index.tsx index 66c2076eed..8a6298bb2b 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/ColumnBlock/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/ColumnBlock/index.tsx @@ -1,13 +1,14 @@ import React from 'react'; -import { TreeNodeInterface } from '$app/interfaces/index'; -import BlockComponent from '../BlockList/BlockComponent'; +import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node'; + +import BlockComponent from '../BlockComponent'; export default function ColumnBlock({ node, resizerWidth, index, }: { - node: TreeNodeInterface; + node: TreeNode; resizerWidth: number; index: number; }) { @@ -16,6 +17,7 @@ export default function ColumnBlock({
); }; + return ( <> {index === 0 ? ( @@ -41,11 +43,8 @@ export default function ColumnBlock({ width: `calc((100% - ${resizerWidth}px) * ${node.data.ratio})`, }} node={node} - > - {node.children?.map((item) => ( - - ))} - + renderChild={(item) => } + /> ); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/HeadingBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/HeadingBlock/index.tsx index a3f47386fb..f0a1bd3323 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/HeadingBlock/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/HeadingBlock/index.tsx @@ -1,21 +1,17 @@ -import React from 'react'; import TextBlock from '../TextBlock'; -import { TreeNodeInterface } from '$app/interfaces/index'; +import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node'; +import { BlockCommonProps } from '@/appflowy_app/interfaces'; const fontSize: Record = { 1: 'mt-8 text-3xl', 2: 'mt-6 text-2xl', 3: 'mt-4 text-xl', }; -export default function HeadingBlock({ node }: { node: TreeNodeInterface }) { + +export default function HeadingBlock({ node, version }: BlockCommonProps) { return (
- +
); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/BulletedListBlock.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/BulletedListBlock.tsx index 8a69d1e3aa..38f5b743ea 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/BulletedListBlock.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/BulletedListBlock.tsx @@ -1,13 +1,13 @@ import { Circle } from '@mui/icons-material'; -import BlockComponent from '../BlockList/BlockComponent'; -import { TreeNodeInterface } from '$app/interfaces/index'; +import BlockComponent from '../BlockComponent'; +import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node'; -export default function BulletedListBlock({ title, node }: { title: JSX.Element; node: TreeNodeInterface }) { +export default function BulletedListBlock({ title, node }: { title: JSX.Element; node: TreeNode }) { return (
-
+
{title} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/ColumnListBlock.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/ColumnListBlock.tsx index 1c2b745229..ce0a1254d3 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/ColumnListBlock.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/ColumnListBlock.tsx @@ -1,8 +1,8 @@ -import { TreeNodeInterface } from '@/appflowy_app/interfaces'; +import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node'; import React, { useMemo } from 'react'; -import ColumnBlock from '../ColumnBlock/index'; +import ColumnBlock from '../ColumnBlock'; -export default function ColumnListBlock({ node }: { node: TreeNodeInterface }) { +export default function ColumnListBlock({ node }: { node: TreeNode }) { const resizerWidth = useMemo(() => { return 46 * (node.children?.length || 0); }, [node.children?.length]); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/NumberedListBlock.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/NumberedListBlock.tsx index 96857b663d..6bc63d41ef 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/NumberedListBlock.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/NumberedListBlock.tsx @@ -1,16 +1,21 @@ -import { TreeNodeInterface } from '@/appflowy_app/interfaces'; -import React, { useMemo } from 'react'; -import BlockComponent from '../BlockList/BlockComponent'; +import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node'; +import BlockComponent from '../BlockComponent'; +import { BlockType } from '@/appflowy_app/interfaces'; +import { Block } from '@/appflowy_app/block_editor/core/block'; -export default function NumberedListBlock({ title, node }: { title: JSX.Element; node: TreeNodeInterface }) { - const index = useMemo(() => { - const i = node.parent?.children?.findIndex((item) => item.id === node.id) || 0; - return i + 1; - }, [node]); +export default function NumberedListBlock({ title, node }: { title: JSX.Element; node: TreeNode }) { + let prev = node.block.prev; + let index = 1; + while (prev && prev.type === BlockType.ListBlock && (prev as Block).data.type === 'numbered') { + index++; + prev = prev.prev; + } return (
-
{`${index} .`}
+
{`${index} .`}
{title}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/index.tsx index b235828ff4..87c31795ce 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/index.tsx @@ -3,22 +3,18 @@ import TextBlock from '../TextBlock'; import NumberedListBlock from './NumberedListBlock'; import BulletedListBlock from './BulletedListBlock'; import ColumnListBlock from './ColumnListBlock'; -import { TreeNodeInterface } from '$app/interfaces/index'; +import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node'; +import { BlockCommonProps } from '@/appflowy_app/interfaces'; -export default function ListBlock({ node }: { node: TreeNodeInterface }) { +export default function ListBlock({ node, version }: BlockCommonProps) { const title = useMemo(() => { if (node.data.type === 'column') return <>; return (
- +
); - }, [node]); + }, [node, version]); if (node.data.type === 'numbered') { return ; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/PageBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/PageBlock/index.tsx index f4a5326916..a79e036dbe 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/PageBlock/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/PageBlock/index.tsx @@ -1,6 +1,6 @@ -import React from 'react'; -import { TreeNodeInterface } from '$app/interfaces'; +import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node'; +import { BlockCommonProps } from '@/appflowy_app/interfaces'; -export default function PageBlock({ node }: { node: TreeNodeInterface }) { +export default function PageBlock({ node }: BlockCommonProps) { return
{node.data.title}
; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.hooks.ts new file mode 100644 index 0000000000..a776ae8be4 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.hooks.ts @@ -0,0 +1,98 @@ +import { TreeNode } from "@/appflowy_app/block_editor/view/tree_node"; +import { triggerHotkey } from "@/appflowy_app/utils/slate/hotkey"; +import { useCallback, useContext, useLayoutEffect, useState } from "react"; +import { Transforms, createEditor, Descendant } from 'slate'; +import { ReactEditor, withReact } from 'slate-react'; +import { TextBlockContext } from '$app/utils/slate/context'; + +export function useTextBlock({ + node, +}: { + node: TreeNode; +}) { + const [editor] = useState(() => withReact(createEditor())); + + const { textBlockManager } = useContext(TextBlockContext); + + const value = [ + { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + type: 'paragraph', + children: node.data.content, + }, + ]; + + + const onChange = useCallback( + (e: Descendant[]) => { + if (!editor.operations || editor.operations.length === 0) return; + if (editor.operations[0].type !== 'set_selection') { + console.log('====text block ==== ', editor.operations) + const children = 'children' in e[0] ? e[0].children : []; + textBlockManager?.update(node, ['data', 'content'], children); + } else { + const newProperties = editor.operations[0].newProperties; + textBlockManager?.setSelection(node, editor.selection); + } + }, + [node.id, editor], + ); + + + const onKeyDownCapture = (event: React.KeyboardEvent) => { + switch (event.key) { + case 'Enter': { + event.stopPropagation(); + event.preventDefault(); + textBlockManager?.splitNode(node, editor); + + return; + } + } + + triggerHotkey(event, editor); + } + + + + const { focusId, selection } = textBlockManager!.selectionManager.getFocusSelection(); + + editor.children = value; + Transforms.collapse(editor); + + useLayoutEffect(() => { + let timer: NodeJS.Timeout; + if (focusId === node.id && selection) { + ReactEditor.focus(editor); + Transforms.select(editor, selection); + // Use setTimeout to delay setting the selection + // until Slate has fully loaded and rendered all components and contents, + // to ensure that the operation succeeds. + timer = setTimeout(() => { + Transforms.select(editor, selection); + }, 100); + } + + return () => timer && clearTimeout(timer) + }, [editor]); + + const onDOMBeforeInput = useCallback((e: InputEvent) => { + // COMPAT: in Apple, `compositionend` is dispatched after the + // `beforeinput` for "insertFromComposition". It will cause repeated characters when inputting Chinese. + // Here, prevent the beforeInput event and wait for the compositionend event to take effect + if (e.inputType === 'insertFromComposition') { + e.preventDefault(); + } + + }, []); + + + return { + editor, + value, + onChange, + onKeyDownCapture, + onDOMBeforeInput, + } +} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.tsx index dd3dbce5de..906e9a4060 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.tsx @@ -1,77 +1,37 @@ -import React, { useContext, useMemo, useState } from 'react'; -import { TreeNodeInterface } from '$app/interfaces'; -import BlockComponent from '../BlockList/BlockComponent'; - -import { createEditor } from 'slate'; -import { Slate, Editable, withReact } from 'slate-react'; +import BlockComponent from '../BlockComponent'; +import { Slate, Editable } from 'slate-react'; import Leaf from './Leaf'; import HoveringToolbar from '$app/components/HoveringToolbar'; -import { triggerHotkey } from '@/appflowy_app/utils/slate/hotkey'; -import { BlockContext } from '$app/utils/block_context'; -import { debounce } from '@/appflowy_app/utils/tool'; -import { getBlockEditor } from '@/appflowy_app/block_editor/index'; +import { TreeNode } from '@/appflowy_app/block_editor/view/tree_node'; +import { useTextBlock } from './index.hooks'; +import { BlockCommonProps, TextBlockToolbarProps } from '@/appflowy_app/interfaces'; +import { toolbarDefaultProps } from '@/appflowy_app/constants/toolbar'; -const INPUT_CHANGE_CACHE_DELAY = 300; - -export default function TextBlock({ node }: { node: TreeNodeInterface }) { - const blockEditor = getBlockEditor(); - if (!blockEditor) return null; - - const [editor] = useState(() => withReact(createEditor())); - - const { id } = useContext(BlockContext); - - const debounceUpdateBlockCache = useMemo( - () => debounce(blockEditor.renderTree.updateNodeRect, INPUT_CHANGE_CACHE_DELAY), - [id, node.id] - ); +export default function TextBlock({ + node, + needRenderChildren = true, + toolbarProps, + ...props +}: { + needRenderChildren?: boolean; + toolbarProps?: TextBlockToolbarProps; +} & BlockCommonProps & + React.HTMLAttributes) { + const { editor, value, onChange, onKeyDownCapture, onDOMBeforeInput } = useTextBlock({ node }); + const { showGroups } = toolbarProps || toolbarDefaultProps; return ( -
- { - if (editor.operations[0].type !== 'set_selection') { - console.log('=== text op ===', e, editor.operations); - // Temporary code, in the future, it is necessary to monitor the OP changes of the document to determine whether the location cache of the block needs to be updated - debounceUpdateBlockCache(node.id); - } - }} - value={[ - { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - type: 'paragraph', - children: node.data.content, - }, - ]} - > - +
+ + {showGroups.length > 0 && } { - switch (event.key) { - case 'Enter': { - event.stopPropagation(); - event.preventDefault(); - return; - } - } - - triggerHotkey(event, editor); - }} - onDOMBeforeInput={(e) => { - // COMPAT: in Apple, `compositionend` is dispatched after the - // `beforeinput` for "insertFromComposition". It will cause repeated characters when inputting Chinese. - // Here, prevent the beforeInput event and wait for the compositionend event to take effect - if (e.inputType === 'insertFromComposition') { - e.preventDefault(); - } - }} - renderLeaf={(props) => } + onKeyDownCapture={onKeyDownCapture} + onDOMBeforeInput={onDOMBeforeInput} + renderLeaf={(leafProps) => } placeholder='Enter some text...' /> - {node.children && node.children.length > 0 ? ( + {needRenderChildren && node.children.length > 0 ? (
{node.children.map((item) => ( diff --git a/frontend/appflowy_tauri/src/appflowy_app/constants/toolbar.ts b/frontend/appflowy_tauri/src/appflowy_app/constants/toolbar.ts index 07cd1d8be9..a0efb98d60 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/constants/toolbar.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/constants/toolbar.ts @@ -1,3 +1,4 @@ +import { TextBlockToolbarGroup } from "../interfaces"; export const iconSize = { width: 18, height: 18 }; @@ -22,4 +23,17 @@ export const command: Record = { title: 'Strike through', key: '⌘ + Shift + S or ⌘ + Shift + X', }, +}; + +export const toolbarDefaultProps = { + showGroups: [ + TextBlockToolbarGroup.ASK_AI, + TextBlockToolbarGroup.BLOCK_SELECT, + TextBlockToolbarGroup.ADD_LINK, + TextBlockToolbarGroup.COMMENT, + TextBlockToolbarGroup.TEXT_FORMAT, + TextBlockToolbarGroup.TEXT_COLOR, + TextBlockToolbarGroup.MENTION, + TextBlockToolbarGroup.MORE, + ], }; \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts b/frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts index 4d91df74a6..e6d0760f64 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts @@ -16,9 +16,7 @@ export enum BlockType { } - - -export type BlockData = T extends BlockType.TextBlock ? TextBlockData : +export type BlockData = T extends BlockType.TextBlock ? TextBlockData : T extends BlockType.PageBlock ? PageBlockData : T extends BlockType.HeadingBlock ? HeadingBlockData : T extends BlockType.ListBlock ? ListBlockData : @@ -34,7 +32,7 @@ export interface BlockInterface { } -interface TextBlockData { +export interface TextBlockData { content: Descendant[]; } @@ -54,11 +52,61 @@ interface ColumnBlockData { ratio: string; } +// eslint-disable-next-line no-shadow +export enum TextBlockToolbarGroup { + ASK_AI, + BLOCK_SELECT, + ADD_LINK, + COMMENT, + TEXT_FORMAT, + TEXT_COLOR, + MENTION, + MORE +} +export interface TextBlockToolbarProps { + showGroups: TextBlockToolbarGroup[] +} -export interface TreeNodeInterface { - id: string; - type: BlockType; - parent: TreeNodeInterface | null; - children: TreeNodeInterface[]; - data: BlockData; + +export interface BlockCommonProps { + version: number; + node: T; +} + +export interface BackendOp { + type: 'update' | 'insert' | 'remove' | 'move' | 'move_range'; + version: number; + data: UpdateOpData | InsertOpData | moveRangeOpData | moveOpData | removeOpData; +} +export interface LocalOp { + type: 'update' | 'insert' | 'remove' | 'move' | 'move_range'; + version: number; + data: UpdateOpData | InsertOpData | moveRangeOpData | moveOpData | removeOpData; +} + +export interface UpdateOpData { + blockId: string; + value: BlockData; + path: string[]; +} +export interface InsertOpData { + block: BlockInterface; + parentId: string; + prevId?: string +} + +export interface moveRangeOpData { + range: [string, string]; + newParentId: string; + newPrevId?: string +} + +export interface moveOpData { + blockId: string; + newParentId: string; + newPrevId?: string +} + +export interface removeOpData { + blockId: string } \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/block.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/block.ts new file mode 100644 index 0000000000..c40e840036 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/block.ts @@ -0,0 +1,25 @@ + +import { createContext } from 'react'; +import { ulid } from "ulid"; +import { BlockEditor } from '../block_editor/index'; + +export const BlockContext = createContext<{ + id?: string; + blockEditor?: BlockEditor; +}>({}); + + +export function generateBlockId() { + const blockId = ulid() + return `block-id-${blockId}`; +} + +const AVERAGE_BLOCK_HEIGHT = 30; +export function calculateViewportBlockMaxCount() { + const viewportHeight = window.innerHeight; + const viewportBlockCount = Math.ceil(viewportHeight / AVERAGE_BLOCK_HEIGHT); + + return viewportBlockCount; +} + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/block_context.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/block_context.ts deleted file mode 100644 index 71b99fd8a2..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/block_context.ts +++ /dev/null @@ -1,8 +0,0 @@ - -import { createContext } from 'react'; - -export const BlockContext = createContext<{ - id?: string; -}>({}); - - diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/block_selection.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/block_selection.ts new file mode 100644 index 0000000000..8bc67522ce --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/block_selection.ts @@ -0,0 +1,36 @@ +import { BlockData, BlockType } from "../interfaces"; + + +export function filterSelections(ids: string[], nodeMap: Map): string[] { + const selected = new Set(ids); + const newSelected = new Set(); + ids.forEach(selectedId => { + const node = nodeMap.get(selectedId); + if (!node) return; + if (node.type === BlockType.ListBlock && node.data.type === 'column') { + return; + } + if (node.children.length === 0) { + newSelected.add(selectedId); + return; + } + const hasChildSelected = node.children.some(i => selected.has(i.id)); + if (!hasChildSelected) { + newSelected.add(selectedId); + return; + } + const hasSiblingSelected = node.parent?.children.filter(i => i.id !== selectedId).some(i => selected.has(i.id)); + if (hasChildSelected && hasSiblingSelected) { + newSelected.add(selectedId); + return; + } + }); + + return Array.from(newSelected); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/editor/format.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/editor/format.ts deleted file mode 100644 index fd36928b76..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/editor/format.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { - Editor, - Transforms, - Text, - Node -} from 'slate'; - -export function toggleFormat(editor: Editor, format: string) { - const isActive = isFormatActive(editor, format) - Transforms.setNodes( - editor, - { [format]: isActive ? null : true }, - { match: Text.isText, split: true } - ) -} - -export const isFormatActive = (editor: Editor, format: string) => { - const [match] = Editor.nodes(editor, { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - match: (n: Node) => n[format] === true, - mode: 'all', - }) - return !!match -} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/editor/hotkey.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/editor/hotkey.ts deleted file mode 100644 index fad418086d..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/editor/hotkey.ts +++ /dev/null @@ -1,22 +0,0 @@ -import isHotkey from 'is-hotkey'; -import { toggleFormat } from './format'; -import { Editor } from 'slate'; - -const HOTKEYS: Record = { - 'mod+b': 'bold', - 'mod+i': 'italic', - 'mod+u': 'underline', - 'mod+e': 'code', - 'mod+shift+X': 'strikethrough', - 'mod+shift+S': 'strikethrough', -}; - -export function triggerHotkey(event: React.KeyboardEvent, editor: Editor) { - for (const hotkey in HOTKEYS) { - if (isHotkey(hotkey, event)) { - event.preventDefault() - const format = HOTKEYS[hotkey] - toggleFormat(editor, format) - } - } -} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/editor/toolbar.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/editor/toolbar.ts deleted file mode 100644 index 80131a4d69..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/editor/toolbar.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Editor, Range } from 'slate'; -export function calcToolbarPosition(editor: Editor, el: HTMLDivElement, blockRect: DOMRect) { - const { selection } = editor; - - if (!selection || Range.isCollapsed(selection) || Editor.string(editor, selection) === '') { - return; - } - - const domSelection = window.getSelection(); - let domRange; - if (domSelection?.rangeCount === 0) { - domRange = document.createRange(); - domRange.setStart(el, domSelection?.anchorOffset); - domRange.setEnd(el, domSelection?.anchorOffset); - } else { - domRange = domSelection?.getRangeAt(0); - } - - const rect = domRange?.getBoundingClientRect() || { top: 0, left: 0, width: 0, height: 0 }; - - const top = `${-el.offsetHeight - 5}px`; - const left = `${rect.left - blockRect.left - el.offsetWidth / 2 + rect.width / 2}px`; - return { - top, - left, - } - -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/slate/context.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/slate/context.ts new file mode 100644 index 0000000000..387b74ff50 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/slate/context.ts @@ -0,0 +1,6 @@ +import { createContext } from "react"; +import { TextBlockManager } from '../../block_editor/blocks/text_block'; + +export const TextBlockContext = createContext<{ + textBlockManager?: TextBlockManager +}>({}); diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/slate/toolbar.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/slate/toolbar.ts index ff0572d278..52681474d5 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/slate/toolbar.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/slate/toolbar.ts @@ -1,21 +1,11 @@ -import { getBlockEditor } from '@/appflowy_app/block_editor'; import { Editor, Range } from 'slate'; -export function calcToolbarPosition(editor: Editor, toolbarDom: HTMLDivElement, blockId: string) { +export function calcToolbarPosition(editor: Editor, toolbarDom: HTMLDivElement, blockRect: DOMRect) { const { selection } = editor; - const scrollContainer = document.querySelector('.doc-scroller-container'); - if (!scrollContainer) return; - if (!selection || Range.isCollapsed(selection) || Editor.string(editor, selection) === '') { return; } - const blockEditor = getBlockEditor(); - const blockRect = blockEditor?.renderTree.getNodeRect(blockId); - const blockDom = document.querySelector(`[data-block-id=${blockId}]`); - - if (!blockDom || !blockRect) return; - const domSelection = window.getSelection(); let domRange; if (domSelection?.rangeCount === 0) { @@ -26,8 +16,8 @@ export function calcToolbarPosition(editor: Editor, toolbarDom: HTMLDivElement, const rect = domRange?.getBoundingClientRect() || { top: 0, left: 0, width: 0, height: 0 }; - const top = `${-toolbarDom.offsetHeight - 5 + (rect.top + scrollContainer.scrollTop - blockRect.top)}px`; - const left = `${rect.left - blockRect.left - toolbarDom.offsetWidth / 2 + rect.width / 2}px`; + const top = `${-toolbarDom.offsetHeight - 5 + (rect.top - blockRect.y)}px`; + const left = `${rect.left - blockRect.x - toolbarDom.offsetWidth / 2 + rect.width / 2}px`; return { top, diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts index a893f2eb0f..88036d82d5 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts @@ -8,3 +8,29 @@ export function debounce(fn: (...args: any[]) => void, delay: number) { }, delay) } } + +export function get(obj: any, path: string[], defaultValue?: any) { + let value = obj; + for (const prop of path) { + value = value[prop]; + if (value === undefined) { + return defaultValue !== undefined ? defaultValue : undefined; + } + } + return value; +} + +export function set(obj: any, path: string[], value: any): void { + let current = obj; + for (let i = 0; i < path.length; i++) { + const prop = path[i]; + if (i === path.length - 1) { + current[prop] = value; + } else { + if (!current[prop]) { + current[prop] = {}; + } + current = current[prop]; + } + } +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts index 6c4bda8e97..1dfe73fd85 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { DocumentEventGetDocument, DocumentVersionPB, @@ -6,14 +6,14 @@ import { } from '../../services/backend/events/flowy-document'; import { BlockInterface, BlockType } from '../interfaces'; import { useParams } from 'react-router-dom'; -import { getBlockEditor, createBlockEditor } from '../block_editor'; +import { BlockEditor } from '../block_editor'; const loadBlockData = async (id: string): Promise> => { return { [id]: { id: id, type: BlockType.PageBlock, - data: { title: 'Document Title' }, + data: { content: [{ text: 'Document Title' }] }, next: null, firstChild: "L1-1", }, @@ -202,26 +202,580 @@ const loadBlockData = async (id: string): Promise next: null, firstChild: null, }, + "L1-8": { + id: "L1-8", + type: BlockType.HeadingBlock, + data: { level: 1, content: [{ text: 'Heading 1' }] }, + next: "L1-9", + firstChild: null, + }, + "L1-9": { + id: "L1-9", + type: BlockType.HeadingBlock, + data: { level: 2, content: [{ text: 'Heading 2' }] }, + next: "L1-10", + firstChild: null, + }, + "L1-10": { + id: "L1-10", + type: BlockType.HeadingBlock, + data: { level: 3, content: [{ text: 'Heading 3' }] }, + next: "L1-11", + firstChild: null, + }, + "L1-11": { + id: "L1-11", + type: BlockType.TextBlock, + data: { content: [ + { + text: + 'This example shows how you can make a hovering menu appear above your content, which you can use to make text ', + }, + { text: 'bold', bold: true }, + { text: ', ' }, + { text: 'italic', italic: true }, + { text: ', or anything else you might want to do!' }, + ] }, + next: "L1-12", + firstChild: null, + }, + "L1-12": { + id: "L1-12", + type: BlockType.TextBlock, + data: { content: [ + { text: 'Try it out yourself! Just ' }, + { text: 'select any piece of text and the menu will appear', bold: true }, + { text: '.' }, + ] }, + next: "L2-1", + firstChild: "L1-12-1", + }, + "L1-12-1": { + id: "L1-12-1", + type: BlockType.TextBlock, + data: { content: [ + { text: 'Try it out yourself! Just ' }, + ] }, + next: "L1-12-2", + firstChild: null, + }, + "L1-12-2": { + id: "L1-12-2", + type: BlockType.TextBlock, + data: { content: [ + { text: 'Try it out yourself! Just ' }, + ] }, + next: null, + firstChild: null, + }, + "L2-1": { + id: "L2-1", + type: BlockType.HeadingBlock, + data: { level: 1, content: [{ text: 'Heading 1' }] }, + next: "L2-2", + firstChild: null, + }, + "L2-2": { + id: "L2-2", + type: BlockType.HeadingBlock, + data: { level: 2, content: [{ text: 'Heading 2' }] }, + next: "L2-3", + firstChild: null, + }, + "L2-3": { + id: "L2-3", + type: BlockType.HeadingBlock, + data: { level: 3, content: [{ text: 'Heading 3' }] }, + next: "L2-4", + firstChild: null, + }, + "L2-4": { + id: "L2-4", + type: BlockType.TextBlock, + data: { content: [ + { + text: + 'This example shows how you can make a hovering menu appear above your content, which you can use to make text ', + }, + { text: 'bold', bold: true }, + { text: ', ' }, + { text: 'italic', italic: true }, + { text: ', or anything else you might want to do!' }, + ] }, + next: "L2-5", + firstChild: null, + }, + "L2-5": { + id: "L2-5", + type: BlockType.TextBlock, + data: { content: [ + { text: 'Try it out yourself! Just ' }, + { text: 'select any piece of text and the menu will appear', bold: true }, + { text: '.' }, + ] }, + next: "L2-6", + firstChild: "L2-5-1", + }, + "L2-5-1": { + id: "L2-5-1", + type: BlockType.TextBlock, + data: { content: [ + { text: 'Try it out yourself! Just ' }, + ] }, + next: "L2-5-2", + firstChild: null, + }, + "L2-5-2": { + id: "L2-5-2", + type: BlockType.TextBlock, + data: { content: [ + { text: 'Try it out yourself! Just ' }, + ] }, + next: null, + firstChild: null, + }, + "L2-6": { + id: "L2-6", + type: BlockType.ListBlock, + data: { type: 'bulleted', content: [ + { + text: + "Since it's rich text, you can do things like turn a selection of text ", + }, + { text: 'bold', bold: true }, + { + text: + ', or add a semantically rendered block quote in the middle of the page, like this:', + }, + ] }, + next: "L2-7", + firstChild: "L2-6-1", + }, + "L2-6-1": { + id: "L2-6-1", + type: BlockType.ListBlock, + data: { type: 'numbered', content: [ + { + text: + "Since it's rich text, you can do things like turn a selection of text ", + }, + + ] }, + + next: "L2-6-2", + firstChild: null, + }, + "L2-6-2": { + id: "L2-6-2", + type: BlockType.ListBlock, + data: { type: 'numbered', content: [ + { + text: + "Since it's rich text, you can do things like turn a selection of text ", + }, + + ] }, + + next: "L2-6-3", + firstChild: null, + }, + + "L2-6-3": { + id: "L2-6-3", + type: BlockType.TextBlock, + data: { content: [{ text: 'A wise quote.' }] }, + next: null, + firstChild: null, + }, + + "L2-7": { + id: "L2-7", + type: BlockType.ListBlock, + data: { type: 'column' }, + + next: "L2-8", + firstChild: "L2-7-1", + }, + "L2-7-1": { + id: "L2-7-1", + type: BlockType.ColumnBlock, + data: { ratio: '0.33' }, + next: "L2-7-2", + firstChild: "L2-7-1-1", + }, + "L2-7-1-1": { + id: "L2-7-1-1", + type: BlockType.TextBlock, + data: { content: [ + { text: 'Try it out yourself! Just ' }, + ] }, + next: null, + firstChild: null, + }, + "L2-7-2": { + id: "L2-7-2", + type: BlockType.ColumnBlock, + data: { ratio: '0.33' }, + next: "L2-7-3", + firstChild: "L2-7-2-1", + }, + "L2-7-2-1": { + id: "L2-7-2-1", + type: BlockType.TextBlock, + data: { content: [ + { text: 'Try it out yourself! Just ' }, + ] }, + next: "L2-7-2-2", + firstChild: null, + }, + "L2-7-2-2": { + id: "L2-7-2-2", + type: BlockType.TextBlock, + data: { content: [ + { text: 'Try it out yourself! Just ' }, + ] }, + next: null, + firstChild: null, + }, + "L2-7-3": { + id: "L2-7-3", + type: BlockType.ColumnBlock, + data: { ratio: '0.33' }, + next: null, + firstChild: "L2-7-3-1", + }, + "L2-7-3-1": { + id: "L2-7-3-1", + type: BlockType.TextBlock, + data: { content: [ + { text: 'Try it out yourself! Just ' }, + ] }, + next: null, + firstChild: null, + }, + "L2-8": { + id: "L2-8", + type: BlockType.HeadingBlock, + data: { level: 1, content: [{ text: 'Heading 1' }] }, + next: "L2-9", + firstChild: null, + }, + "L2-9": { + id: "L2-9", + type: BlockType.HeadingBlock, + data: { level: 2, content: [{ text: 'Heading 2' }] }, + next: "L2-10", + firstChild: null, + }, + "L2-10": { + id: "L2-10", + type: BlockType.HeadingBlock, + data: { level: 3, content: [{ text: 'Heading 3' }] }, + next: "L2-11", + firstChild: null, + }, + "L2-11": { + id: "L2-11", + type: BlockType.TextBlock, + data: { content: [ + { + text: + 'This example shows how you can make a hovering menu appear above your content, which you can use to make text ', + }, + { text: 'bold', bold: true }, + { text: ', ' }, + { text: 'italic', italic: true }, + { text: ', or anything else you might want to do!' }, + ] }, + next: "L2-12", + firstChild: null, + }, + "L2-12": { + id: "L2-12", + type: BlockType.TextBlock, + data: { content: [ + { text: 'Try it out yourself! Just ' }, + { text: 'select any piece of text and the menu will appear', bold: true }, + { text: '.' }, + ] }, + next: "L3-1", + firstChild: "L2-12-1", + }, + "L2-12-1": { + id: "L2-12-1", + type: BlockType.TextBlock, + data: { content: [ + { text: 'Try it out yourself! Just ' }, + ] }, + next: "L2-12-2", + firstChild: null, + }, + "L2-12-2": { + id: "L2-12-2", + type: BlockType.TextBlock, + data: { content: [ + { text: 'Try it out yourself! Just ' }, + ] }, + next: null, + firstChild: null, + },"L3-1": { + id: "L3-1", + type: BlockType.HeadingBlock, + data: { level: 1, content: [{ text: 'Heading 1' }] }, + next: "L3-2", + firstChild: null, + }, + "L3-2": { + id: "L3-2", + type: BlockType.HeadingBlock, + data: { level: 2, content: [{ text: 'Heading 2' }] }, + next: "L3-3", + firstChild: null, + }, + "L3-3": { + id: "L3-3", + type: BlockType.HeadingBlock, + data: { level: 3, content: [{ text: 'Heading 3' }] }, + next: "L3-4", + firstChild: null, + }, + "L3-4": { + id: "L3-4", + type: BlockType.TextBlock, + data: { content: [ + { + text: + 'This example shows how you can make a hovering menu appear above your content, which you can use to make text ', + }, + { text: 'bold', bold: true }, + { text: ', ' }, + { text: 'italic', italic: true }, + { text: ', or anything else you might want to do!' }, + ] }, + next: "L3-5", + firstChild: null, + }, + "L3-5": { + id: "L3-5", + type: BlockType.TextBlock, + data: { content: [ + { text: 'Try it out yourself! Just ' }, + { text: 'select any piece of text and the menu will appear', bold: true }, + { text: '.' }, + ] }, + next: "L3-6", + firstChild: "L3-5-1", + }, + "L3-5-1": { + id: "L3-5-1", + type: BlockType.TextBlock, + data: { content: [ + { text: 'Try it out yourself! Just ' }, + ] }, + next: "L3-5-2", + firstChild: null, + }, + "L3-5-2": { + id: "L3-5-2", + type: BlockType.TextBlock, + data: { content: [ + { text: 'Try it out yourself! Just ' }, + ] }, + next: null, + firstChild: null, + }, + "L3-6": { + id: "L3-6", + type: BlockType.ListBlock, + data: { type: 'bulleted', content: [ + { + text: + "Since it's rich text, you can do things like turn a selection of text ", + }, + { text: 'bold', bold: true }, + { + text: + ', or add a semantically rendered block quote in the middle of the page, like this:', + }, + ] }, + next: "L3-7", + firstChild: "L3-6-1", + }, + "L3-6-1": { + id: "L3-6-1", + type: BlockType.ListBlock, + data: { type: 'numbered', content: [ + { + text: + "Since it's rich text, you can do things like turn a selection of text ", + }, + + ] }, + + next: "L3-6-2", + firstChild: null, + }, + "L3-6-2": { + id: "L3-6-2", + type: BlockType.ListBlock, + data: { type: 'numbered', content: [ + { + text: + "Since it's rich text, you can do things like turn a selection of text ", + }, + + ] }, + + next: "L3-6-3", + firstChild: null, + }, + + "L3-6-3": { + id: "L3-6-3", + type: BlockType.TextBlock, + data: { content: [{ text: 'A wise quote.' }] }, + next: null, + firstChild: null, + }, + + "L3-7": { + id: "L3-7", + type: BlockType.ListBlock, + data: { type: 'column' }, + + next: "L3-8", + firstChild: "L3-7-1", + }, + "L3-7-1": { + id: "L3-7-1", + type: BlockType.ColumnBlock, + data: { ratio: '0.33' }, + next: "L3-7-2", + firstChild: "L3-7-1-1", + }, + "L3-7-1-1": { + id: "L3-7-1-1", + type: BlockType.TextBlock, + data: { content: [ + { text: 'Try it out yourself! Just ' }, + ] }, + next: null, + firstChild: null, + }, + "L3-7-2": { + id: "L3-7-2", + type: BlockType.ColumnBlock, + data: { ratio: '0.33' }, + next: "L3-7-3", + firstChild: "L3-7-2-1", + }, + "L3-7-2-1": { + id: "L3-7-2-1", + type: BlockType.TextBlock, + data: { content: [ + { text: 'Try it out yourself! Just ' }, + ] }, + next: "L3-7-2-2", + firstChild: null, + }, + "L3-7-2-2": { + id: "L3-7-2-2", + type: BlockType.TextBlock, + data: { content: [ + { text: 'Try it out yourself! Just ' }, + ] }, + next: null, + firstChild: null, + }, + "L3-7-3": { + id: "L3-7-3", + type: BlockType.ColumnBlock, + data: { ratio: '0.33' }, + next: null, + firstChild: "L3-7-3-1", + }, + "L3-7-3-1": { + id: "L3-7-3-1", + type: BlockType.TextBlock, + data: { content: [ + { text: 'Try it out yourself! Just ' }, + ] }, + next: null, + firstChild: null, + }, + "L3-8": { + id: "L3-8", + type: BlockType.HeadingBlock, + data: { level: 1, content: [{ text: 'Heading 1' }] }, + next: "L3-9", + firstChild: null, + }, + "L3-9": { + id: "L3-9", + type: BlockType.HeadingBlock, + data: { level: 2, content: [{ text: 'Heading 2' }] }, + next: "L3-10", + firstChild: null, + }, + "L3-10": { + id: "L3-10", + type: BlockType.HeadingBlock, + data: { level: 3, content: [{ text: 'Heading 3' }] }, + next: "L3-11", + firstChild: null, + }, + "L3-11": { + id: "L3-11", + type: BlockType.TextBlock, + data: { content: [ + { + text: + 'This example shows how you can make a hovering menu appear above your content, which you can use to make text ', + }, + { text: 'bold', bold: true }, + { text: ', ' }, + { text: 'italic', italic: true }, + { text: ', or anything else you might want to do!' }, + ] }, + next: "L3-12", + firstChild: null, + }, + "L3-12": { + id: "L3-12", + type: BlockType.TextBlock, + data: { content: [ + { text: 'Try it out yourself! Just ' }, + { text: 'select any piece of text and the menu will appear', bold: true }, + { text: '.' }, + ] }, + next: null, + firstChild: "L3-12-1", + }, + "L3-12-1": { + id: "L3-12-1", + type: BlockType.TextBlock, + data: { content: [ + { text: 'Try it out yourself! Just ' }, + ] }, + next: "L3-12-2", + firstChild: null, + }, + "L3-12-2": { + id: "L3-12-2", + type: BlockType.TextBlock, + data: { content: [ + { text: 'Try it out yourself! Just ' }, + ] }, + next: null, + firstChild: null, + }, } } export const useDocument = () => { const params = useParams(); const [blockId, setBlockId] = useState(); - const loadDocument = async (id: string): Promise => { - const getDocumentResult = await DocumentEventGetDocument( - OpenDocumentPayloadPB.fromObject({ - document_id: id, - version: DocumentVersionPB.V1, - }) - ); + const blockEditorRef = useRef(null) - if (getDocumentResult.ok) { - const pb = getDocumentResult.val; - return JSON.parse(pb.content); - } else { - throw new Error('get document error'); - } - }; useEffect(() => { void (async () => { @@ -229,11 +783,10 @@ export const useDocument = () => { const data = await loadBlockData(params.id); console.log('==== enter ====', params?.id, data); - const blockEditor = getBlockEditor(); - if (blockEditor) { - blockEditor.changeDoc(params?.id, data); + if (!blockEditorRef.current) { + blockEditorRef.current = new BlockEditor(params?.id, data); } else { - createBlockEditor(params?.id, data); + blockEditorRef.current.changeDoc(params?.id, data); } setBlockId(params.id) @@ -242,5 +795,5 @@ export const useDocument = () => { console.log('==== leave ====', params?.id) } }, [params.id]); - return { blockId }; + return { blockId, blockEditor: blockEditorRef.current }; }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx index 5bf71870da..8ab2e71b07 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx @@ -1,6 +1,6 @@ import { useDocument } from './DocumentPage.hooks'; import BlockList from '../components/block/BlockList'; -import { BlockContext } from '../utils/block_context'; +import { BlockContext } from '../utils/block'; import { createTheme, ThemeProvider } from '@mui/material'; const theme = createTheme({ @@ -9,20 +9,19 @@ const theme = createTheme({ }, }); export const DocumentPage = () => { - const { blockId } = useDocument(); + const { blockId, blockEditor } = useDocument(); - if (!blockId) return
; + if (!blockId || !blockEditor) return
; return ( -
- - - -
+ + +
); };