From 9864985befc13ba37cb96d46d33ae7eb462b04f2 Mon Sep 17 00:00:00 2001 From: Rico Kahler Date: Tue, 2 Jul 2024 12:10:02 -0500 Subject: [PATCH] refactor: use `@portabletext/editor` instead of `@sanity/portable-text-editor` (#7035) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: replace internal PTE pkg with public @portabletext/editor pkg (#7001) All the code in `packages/@sanity/portable-text-editor` has been moved to a new `@portabletext/editor` package published on npm. The package lives in https://github.com/portabletext/editor where you'll be able to see that all the git history for these files has been preserved. The goal of publishing this as a standalone package is to eventually allow the Portable Text Editor to be used in other apps, innovate on the editor independently of the Studio and truly make the Portable Text spec feel open. No code changes have been made in that package yet, so replacing the internal code with `v1.0.2` of the package *should* be safe and not accidentally delete any new work or cause regressions. * fix(deps): drop @sanity/portable-text-editor dependency (#7033) * fix(deps): use `^` for `@portabletext/editor` * chore(deps): bump @portabletext/editor in test-studio --------- Co-authored-by: Christian Grøngaard Co-authored-by: Espen Hovlandsdal --- .github/CODEOWNERS | 1 - .github/renovate.json | 5 - .github/workflows/e2e-pte.yml | 74 - dev/aliases.cjs | 1 - dev/test-next-studio/next.config.mjs | 5 - dev/test-studio/package.json | 2 +- .../customMarkers/CustomContentInput.tsx | 2 +- .../customMarkers/blockActions.tsx | 2 +- dev/tsconfig.dev.json | 1 - examples/tsconfig.json | 1 - packages/@repo/test-exports/.depcheckrc.json | 1 - packages/@repo/test-exports/package.json | 1 - .../portable-text-editor/.depcheckrc.json | 3 - .../portable-text-editor/.eslintrc.cjs | 11 - .../@sanity/portable-text-editor/.gitignore | 13 - packages/@sanity/portable-text-editor/LICENSE | 21 - .../@sanity/portable-text-editor/README.md | 3 - .../e2e-tests/__tests__/.eslintrc | 10 - .../__tests__/pasting.collaborative.test.ts | 91 -- .../selectionAdjustment.collaborative.test.ts | 571 ------- .../__tests__/undoRedo.collborative.test.ts | 1412 ----------------- .../writingTogether.collaborative.test.ts | 859 ---------- .../e2e-tests/e2e.config.cjs | 13 - .../portable-text-editor/e2e-tests/schema.ts | 34 - .../portable-text-editor/e2e-tests/serve.ts | 10 - .../e2e-tests/setup/afterEnv.ts | 5 - .../e2e-tests/setup/collaborative.jest.env.ts | 347 ---- .../e2e-tests/setup/globalSetup.ts | 25 - .../e2e-tests/setup/globalTeardown.ts | 5 - .../e2e-tests/setup/globals.jest.ts | 27 - .../e2e-tests/tsconfig.json | 12 - .../e2e-tests/web-server/app.tsx | 120 -- .../web-server/components/Editor.tsx | 239 --- .../e2e-tests/web-server/components/Value.tsx | 27 - .../e2e-tests/web-server/entry.tsx | 11 - .../e2e-tests/web-server/index.html | 12 - .../e2e-tests/web-server/keyGenerator.ts | 11 - .../e2e-tests/web-server/vite.config.js | 24 - .../e2e-tests/ws-server/index.ts | 127 -- .../portable-text-editor/jest.config.cjs | 8 - .../portable-text-editor/package.config.ts | 4 - .../@sanity/portable-text-editor/package.json | 114 -- .../src/editor/Editable.tsx | 683 -------- .../src/editor/PortableTextEditor.tsx | 308 ---- .../__tests__/PortableTextEditor.test.tsx | 386 ----- .../__tests__/PortableTextEditorTester.tsx | 116 -- .../__tests__/RangeDecorations.test.tsx | 115 -- .../src/editor/__tests__/handleClick.test.tsx | 218 --- .../__tests__/pteWarningsSelfSolving.test.tsx | 389 ----- .../src/editor/__tests__/utils.ts | 39 - .../src/editor/components/DraggableBlock.tsx | 287 ---- .../src/editor/components/Element.tsx | 279 ---- .../src/editor/components/Leaf.tsx | 288 ---- .../src/editor/components/SlateContainer.tsx | 81 - .../src/editor/components/Synchronizer.tsx | 190 --- .../src/editor/hooks/usePortableTextEditor.ts | 23 - .../usePortableTextEditorKeyGenerator.ts | 24 - .../hooks/usePortableTextEditorSelection.ts | 22 - .../hooks/usePortableTextEditorValue.ts | 16 - .../editor/hooks/usePortableTextReadOnly.ts | 20 - .../src/editor/hooks/useSyncValue.test.tsx | 125 -- .../src/editor/hooks/useSyncValue.ts | 372 ----- .../src/editor/nodes/DefaultAnnotation.tsx | 16 - .../src/editor/nodes/DefaultObject.tsx | 15 - .../src/editor/nodes/index.ts | 189 --- .../__tests__/withEditableAPIDelete.test.tsx | 244 --- .../withEditableAPIGetFragment.test.tsx | 142 -- .../__tests__/withEditableAPIInsert.test.tsx | 346 ---- ...hEditableAPISelectionsOverlapping.test.tsx | 162 -- .../plugins/__tests__/withHotkeys.test.tsx | 212 --- .../__tests__/withInsertBreak.test.tsx | 204 --- .../__tests__/withPlaceholderBlock.test.tsx | 133 -- .../__tests__/withPortableTextLists.test.tsx | 65 - .../withPortableTextMarkModel.test.tsx | 1377 ---------------- .../withPortableTextSelections.test.tsx | 91 -- .../plugins/__tests__/withUndoRedo.test.tsx | 115 -- .../editor/plugins/createWithEditableAPI.ts | 573 ------- .../src/editor/plugins/createWithHotKeys.ts | 304 ---- .../editor/plugins/createWithInsertBreak.ts | 45 - .../editor/plugins/createWithInsertData.ts | 359 ----- .../src/editor/plugins/createWithMaxBlocks.ts | 24 - .../editor/plugins/createWithObjectKeys.ts | 63 - .../src/editor/plugins/createWithPatches.ts | 274 ---- .../plugins/createWithPlaceholderBlock.ts | 36 - .../createWithPortableTextBlockStyle.ts | 91 -- .../plugins/createWithPortableTextLists.ts | 160 -- .../createWithPortableTextMarkModel.ts | 441 ----- .../createWithPortableTextSelections.ts | 65 - .../editor/plugins/createWithSchemaTypes.ts | 76 - .../src/editor/plugins/createWithUndoRedo.ts | 494 ------ .../src/editor/plugins/createWithUtils.ts | 81 - .../src/editor/plugins/index.ts | 155 -- .../@sanity/portable-text-editor/src/index.ts | 11 - .../src/patch/PatchEvent.ts | 33 - .../src/patch/applyPatch.ts | 29 - .../portable-text-editor/src/patch/array.ts | 89 -- .../src/patch/arrayInsert.ts | 27 - .../portable-text-editor/src/patch/object.ts | 39 - .../portable-text-editor/src/patch/patches.ts | 53 - .../src/patch/primitive.ts | 43 - .../portable-text-editor/src/patch/string.ts | 51 - .../portable-text-editor/src/types/editor.ts | 576 ------- .../portable-text-editor/src/types/options.ts | 17 - .../portable-text-editor/src/types/patch.ts | 65 - .../portable-text-editor/src/types/slate.ts | 25 - .../utils/__tests__/dmpToOperations.test.ts | 181 --- .../__tests__/operationToPatches.test.ts | 421 ----- .../utils/__tests__/patchToOperations.test.ts | 293 ---- .../src/utils/__tests__/ranges.test.ts | 18 - .../__tests__/valueNormalization.test.tsx | 62 - .../src/utils/__tests__/values.test.ts | 253 --- .../src/utils/applyPatch.ts | 407 ----- .../src/utils/bufferUntil.ts | 15 - .../portable-text-editor/src/utils/debug.ts | 12 - .../utils/getPortableTextMemberSchemaTypes.ts | 100 -- .../src/utils/operationToPatches.ts | 357 ----- .../portable-text-editor/src/utils/patches.ts | 36 - .../portable-text-editor/src/utils/paths.ts | 60 - .../portable-text-editor/src/utils/ranges.ts | 77 - .../portable-text-editor/src/utils/schema.ts | 8 - .../src/utils/selection.ts | 65 - .../src/utils/ucs2Indices.ts | 67 - .../src/utils/validateValue.ts | 394 ----- .../portable-text-editor/src/utils/values.ts | 208 --- .../src/utils/weakMaps.ts | 24 - .../src/utils/withChanges.ts | 25 - .../src/utils/withPreserveKeys.ts | 14 - .../src/utils/withoutPatching.ts | 14 - .../portable-text-editor/tsconfig.json | 25 - .../portable-text-editor/tsconfig.lib.json | 10 - packages/@sanity/vision/.depcheckrc.json | 1 - packages/@sanity/vision/package.json | 1 - packages/@sanity/vision/tsconfig.json | 2 - packages/sanity/package.json | 2 +- .../inputs/PortableText/Input.spec.tsx | 2 +- .../inputs/PortableText/InputStory.tsx | 2 +- .../PortableText/RangeDecorationStory.tsx | 2 +- .../CommentInlineHighlightDebugStory.tsx | 2 +- .../pte/comment-input/CommentInput.tsx | 2 +- .../pte/comment-input/CommentInputInner.tsx | 2 +- .../comment-input/CommentInputProvider.tsx | 6 +- .../components/pte/comment-input/Editable.tsx | 2 +- .../components/pte/render/renderBlock.tsx | 2 +- .../components/pte/render/renderChild.tsx | 2 +- .../components/CommentsPortableTextInput.tsx | 2 +- ...ldRangeDecorationSelectionsFromComments.ts | 2 +- .../inline-comments/buildRangeDecorations.tsx | 5 +- .../buildTextSelectionFromFragment.ts | 2 +- .../form/inputs/PortableText/BlockActions.tsx | 2 +- .../form/inputs/PortableText/Compositor.tsx | 2 +- .../core/form/inputs/PortableText/Editor.tsx | 2 +- .../form/inputs/PortableText/InvalidValue.tsx | 2 +- .../inputs/PortableText/PortableTextInput.tsx | 2 +- .../__workshop__/PresenceInputStory.tsx | 2 +- .../customSchema/blockActions.tsx | 2 +- .../__workshop__/customSchema/values.ts | 2 +- .../inputs/PortableText/hooks/useHotKeys.tsx | 2 +- .../hooks/useScrollSelectionIntoView.tsx | 2 +- .../PortableText/hooks/useSpellCheck.tsx | 2 +- .../PortableText/hooks/useTrackFocusPath.tsx | 2 +- .../inputs/PortableText/object/Annotation.tsx | 2 +- .../PortableText/object/BlockObject.tsx | 6 +- .../PortableText/object/InlineObject.tsx | 6 +- .../usePresenceCursorDecorations.tsx | 5 +- .../inputs/PortableText/text/Decorator.tsx | 2 +- .../inputs/PortableText/text/ListItem.tsx | 2 +- .../form/inputs/PortableText/text/Style.tsx | 2 +- .../inputs/PortableText/text/TextBlock.tsx | 6 +- .../PortableText/toolbar/ActionMenu.tsx | 2 +- .../PortableText/toolbar/BlockStyleSelect.tsx | 2 +- .../PortableText/toolbar/InsertMenu.tsx | 2 +- .../inputs/PortableText/toolbar/Toolbar.tsx | 4 +- .../inputs/PortableText/toolbar/helpers.tsx | 10 +- .../form/inputs/PortableText/toolbar/hooks.ts | 2 +- .../form/inputs/PortableText/toolbar/types.ts | 2 +- .../sanity/src/core/form/types/inputProps.ts | 2 +- packages/sanity/src/core/presence/types.ts | 2 +- .../src/core/store/_legacy/presence/types.ts | 2 +- .../descriptionInput/render/renderBlock.tsx | 2 +- packages/sanity/tsconfig.json | 2 - pnpm-lock.yaml | 709 ++------- 181 files changed, 145 insertions(+), 19696 deletions(-) delete mode 100644 .github/workflows/e2e-pte.yml delete mode 100644 packages/@sanity/portable-text-editor/.depcheckrc.json delete mode 100644 packages/@sanity/portable-text-editor/.eslintrc.cjs delete mode 100644 packages/@sanity/portable-text-editor/.gitignore delete mode 100644 packages/@sanity/portable-text-editor/LICENSE delete mode 100644 packages/@sanity/portable-text-editor/README.md delete mode 100644 packages/@sanity/portable-text-editor/e2e-tests/__tests__/.eslintrc delete mode 100644 packages/@sanity/portable-text-editor/e2e-tests/__tests__/pasting.collaborative.test.ts delete mode 100644 packages/@sanity/portable-text-editor/e2e-tests/__tests__/selectionAdjustment.collaborative.test.ts delete mode 100644 packages/@sanity/portable-text-editor/e2e-tests/__tests__/undoRedo.collborative.test.ts delete mode 100644 packages/@sanity/portable-text-editor/e2e-tests/__tests__/writingTogether.collaborative.test.ts delete mode 100644 packages/@sanity/portable-text-editor/e2e-tests/e2e.config.cjs delete mode 100644 packages/@sanity/portable-text-editor/e2e-tests/schema.ts delete mode 100644 packages/@sanity/portable-text-editor/e2e-tests/serve.ts delete mode 100644 packages/@sanity/portable-text-editor/e2e-tests/setup/afterEnv.ts delete mode 100644 packages/@sanity/portable-text-editor/e2e-tests/setup/collaborative.jest.env.ts delete mode 100644 packages/@sanity/portable-text-editor/e2e-tests/setup/globalSetup.ts delete mode 100644 packages/@sanity/portable-text-editor/e2e-tests/setup/globalTeardown.ts delete mode 100644 packages/@sanity/portable-text-editor/e2e-tests/setup/globals.jest.ts delete mode 100644 packages/@sanity/portable-text-editor/e2e-tests/tsconfig.json delete mode 100644 packages/@sanity/portable-text-editor/e2e-tests/web-server/app.tsx delete mode 100644 packages/@sanity/portable-text-editor/e2e-tests/web-server/components/Editor.tsx delete mode 100644 packages/@sanity/portable-text-editor/e2e-tests/web-server/components/Value.tsx delete mode 100644 packages/@sanity/portable-text-editor/e2e-tests/web-server/entry.tsx delete mode 100644 packages/@sanity/portable-text-editor/e2e-tests/web-server/index.html delete mode 100644 packages/@sanity/portable-text-editor/e2e-tests/web-server/keyGenerator.ts delete mode 100644 packages/@sanity/portable-text-editor/e2e-tests/web-server/vite.config.js delete mode 100644 packages/@sanity/portable-text-editor/e2e-tests/ws-server/index.ts delete mode 100644 packages/@sanity/portable-text-editor/jest.config.cjs delete mode 100644 packages/@sanity/portable-text-editor/package.config.ts delete mode 100644 packages/@sanity/portable-text-editor/package.json delete mode 100644 packages/@sanity/portable-text-editor/src/editor/Editable.tsx delete mode 100644 packages/@sanity/portable-text-editor/src/editor/PortableTextEditor.tsx delete mode 100644 packages/@sanity/portable-text-editor/src/editor/__tests__/PortableTextEditor.test.tsx delete mode 100644 packages/@sanity/portable-text-editor/src/editor/__tests__/PortableTextEditorTester.tsx delete mode 100644 packages/@sanity/portable-text-editor/src/editor/__tests__/RangeDecorations.test.tsx delete mode 100644 packages/@sanity/portable-text-editor/src/editor/__tests__/handleClick.test.tsx delete mode 100644 packages/@sanity/portable-text-editor/src/editor/__tests__/pteWarningsSelfSolving.test.tsx delete mode 100644 packages/@sanity/portable-text-editor/src/editor/__tests__/utils.ts delete mode 100644 packages/@sanity/portable-text-editor/src/editor/components/DraggableBlock.tsx delete mode 100644 packages/@sanity/portable-text-editor/src/editor/components/Element.tsx delete mode 100644 packages/@sanity/portable-text-editor/src/editor/components/Leaf.tsx delete mode 100644 packages/@sanity/portable-text-editor/src/editor/components/SlateContainer.tsx delete mode 100644 packages/@sanity/portable-text-editor/src/editor/components/Synchronizer.tsx delete mode 100644 packages/@sanity/portable-text-editor/src/editor/hooks/usePortableTextEditor.ts delete mode 100644 packages/@sanity/portable-text-editor/src/editor/hooks/usePortableTextEditorKeyGenerator.ts delete mode 100644 packages/@sanity/portable-text-editor/src/editor/hooks/usePortableTextEditorSelection.ts delete mode 100644 packages/@sanity/portable-text-editor/src/editor/hooks/usePortableTextEditorValue.ts delete mode 100644 packages/@sanity/portable-text-editor/src/editor/hooks/usePortableTextReadOnly.ts delete mode 100644 packages/@sanity/portable-text-editor/src/editor/hooks/useSyncValue.test.tsx delete mode 100644 packages/@sanity/portable-text-editor/src/editor/hooks/useSyncValue.ts delete mode 100644 packages/@sanity/portable-text-editor/src/editor/nodes/DefaultAnnotation.tsx delete mode 100644 packages/@sanity/portable-text-editor/src/editor/nodes/DefaultObject.tsx delete mode 100644 packages/@sanity/portable-text-editor/src/editor/nodes/index.ts delete mode 100644 packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withEditableAPIDelete.test.tsx delete mode 100644 packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withEditableAPIGetFragment.test.tsx delete mode 100644 packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withEditableAPIInsert.test.tsx delete mode 100644 packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withEditableAPISelectionsOverlapping.test.tsx delete mode 100644 packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withHotkeys.test.tsx delete mode 100644 packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withInsertBreak.test.tsx delete mode 100644 packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withPlaceholderBlock.test.tsx delete mode 100644 packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withPortableTextLists.test.tsx delete mode 100644 packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withPortableTextMarkModel.test.tsx delete mode 100644 packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withPortableTextSelections.test.tsx delete mode 100644 packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withUndoRedo.test.tsx delete mode 100644 packages/@sanity/portable-text-editor/src/editor/plugins/createWithEditableAPI.ts delete mode 100644 packages/@sanity/portable-text-editor/src/editor/plugins/createWithHotKeys.ts delete mode 100644 packages/@sanity/portable-text-editor/src/editor/plugins/createWithInsertBreak.ts delete mode 100644 packages/@sanity/portable-text-editor/src/editor/plugins/createWithInsertData.ts delete mode 100644 packages/@sanity/portable-text-editor/src/editor/plugins/createWithMaxBlocks.ts delete mode 100644 packages/@sanity/portable-text-editor/src/editor/plugins/createWithObjectKeys.ts delete mode 100644 packages/@sanity/portable-text-editor/src/editor/plugins/createWithPatches.ts delete mode 100644 packages/@sanity/portable-text-editor/src/editor/plugins/createWithPlaceholderBlock.ts delete mode 100644 packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextBlockStyle.ts delete mode 100644 packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextLists.ts delete mode 100644 packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextMarkModel.ts delete mode 100644 packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextSelections.ts delete mode 100644 packages/@sanity/portable-text-editor/src/editor/plugins/createWithSchemaTypes.ts delete mode 100644 packages/@sanity/portable-text-editor/src/editor/plugins/createWithUndoRedo.ts delete mode 100644 packages/@sanity/portable-text-editor/src/editor/plugins/createWithUtils.ts delete mode 100644 packages/@sanity/portable-text-editor/src/editor/plugins/index.ts delete mode 100644 packages/@sanity/portable-text-editor/src/index.ts delete mode 100644 packages/@sanity/portable-text-editor/src/patch/PatchEvent.ts delete mode 100644 packages/@sanity/portable-text-editor/src/patch/applyPatch.ts delete mode 100644 packages/@sanity/portable-text-editor/src/patch/array.ts delete mode 100644 packages/@sanity/portable-text-editor/src/patch/arrayInsert.ts delete mode 100644 packages/@sanity/portable-text-editor/src/patch/object.ts delete mode 100644 packages/@sanity/portable-text-editor/src/patch/patches.ts delete mode 100644 packages/@sanity/portable-text-editor/src/patch/primitive.ts delete mode 100644 packages/@sanity/portable-text-editor/src/patch/string.ts delete mode 100644 packages/@sanity/portable-text-editor/src/types/editor.ts delete mode 100644 packages/@sanity/portable-text-editor/src/types/options.ts delete mode 100644 packages/@sanity/portable-text-editor/src/types/patch.ts delete mode 100644 packages/@sanity/portable-text-editor/src/types/slate.ts delete mode 100644 packages/@sanity/portable-text-editor/src/utils/__tests__/dmpToOperations.test.ts delete mode 100644 packages/@sanity/portable-text-editor/src/utils/__tests__/operationToPatches.test.ts delete mode 100644 packages/@sanity/portable-text-editor/src/utils/__tests__/patchToOperations.test.ts delete mode 100644 packages/@sanity/portable-text-editor/src/utils/__tests__/ranges.test.ts delete mode 100644 packages/@sanity/portable-text-editor/src/utils/__tests__/valueNormalization.test.tsx delete mode 100644 packages/@sanity/portable-text-editor/src/utils/__tests__/values.test.ts delete mode 100644 packages/@sanity/portable-text-editor/src/utils/applyPatch.ts delete mode 100644 packages/@sanity/portable-text-editor/src/utils/bufferUntil.ts delete mode 100644 packages/@sanity/portable-text-editor/src/utils/debug.ts delete mode 100644 packages/@sanity/portable-text-editor/src/utils/getPortableTextMemberSchemaTypes.ts delete mode 100644 packages/@sanity/portable-text-editor/src/utils/operationToPatches.ts delete mode 100644 packages/@sanity/portable-text-editor/src/utils/patches.ts delete mode 100644 packages/@sanity/portable-text-editor/src/utils/paths.ts delete mode 100644 packages/@sanity/portable-text-editor/src/utils/ranges.ts delete mode 100644 packages/@sanity/portable-text-editor/src/utils/schema.ts delete mode 100644 packages/@sanity/portable-text-editor/src/utils/selection.ts delete mode 100644 packages/@sanity/portable-text-editor/src/utils/ucs2Indices.ts delete mode 100644 packages/@sanity/portable-text-editor/src/utils/validateValue.ts delete mode 100644 packages/@sanity/portable-text-editor/src/utils/values.ts delete mode 100644 packages/@sanity/portable-text-editor/src/utils/weakMaps.ts delete mode 100644 packages/@sanity/portable-text-editor/src/utils/withChanges.ts delete mode 100644 packages/@sanity/portable-text-editor/src/utils/withPreserveKeys.ts delete mode 100644 packages/@sanity/portable-text-editor/src/utils/withoutPatching.ts delete mode 100644 packages/@sanity/portable-text-editor/tsconfig.json delete mode 100644 packages/@sanity/portable-text-editor/tsconfig.lib.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7bcf59ede15..236179f9fb4 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -17,7 +17,6 @@ /packages/sanity/src/core/preview/ @sanity-io/studio-dx # -- PTE -- -/packages/@sanity/portable-text-editor/ @sanity-io/studio-ex /packages/sanity/src/core/field/types/portableText/ @sanity-io/studio-ex /packages/sanity/src/core/form/inputs/PortableText/ @sanity-io/studio-ex /packages/sanity/src/core/form/types/blockProps.ts @sanity-io/studio-ex diff --git a/.github/renovate.json b/.github/renovate.json index b9bc3520bb3..63d830f22da 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -12,11 +12,6 @@ "matchUpdateTypes": ["minor", "patch"], "automerge": true }, - { - "description": "Slate upgrades are handled manually as they require extensive manual testing to verify it's safe to upgrade", - "matchPackageNames": ["slate", "slate-react"], - "enabled": false - }, { "description": "Dependency updates to examples and the root should always use the chore scope as they aren't published to npm", "matchFileNames": ["package.json", "dev/**/package.json", "examples/**/package.json"], diff --git a/.github/workflows/e2e-pte.yml b/.github/workflows/e2e-pte.yml deleted file mode 100644 index 1cbb8484c51..00000000000 --- a/.github/workflows/e2e-pte.yml +++ /dev/null @@ -1,74 +0,0 @@ -name: End-to-End PTE collaboration tests -on: - # Build on pushes branches that have a PR (including drafts) - pull_request: - # Build on commits pushed to branches without a PR if it's in the allowlist - push: - branches: [next] -jobs: - playwright-test: - timeout-minutes: 30 - runs-on: ubuntu-latest - env: - TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} - TURBO_TEAM: ${{ vars.TURBO_TEAM }} - strategy: - fail-fast: false - matrix: - project: [chromium] - # Add more shards here if needed - shardIndex: [1, 2] - shardTotal: [2] - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 18 - - - uses: pnpm/action-setup@v4 - name: Install pnpm - id: pnpm-install - with: - run_install: false - - - name: Get pnpm store directory - id: pnpm-cache - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT - - - name: Cache node modules - id: cache-node-modules - uses: actions/cache@v4 - env: - cache-name: cache-node-modules - with: - path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-${{ env.cache-name }}-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - v1-${{ runner.os }}-pnpm-store-${{ env.cache-name }}- - v1-${{ runner.os }}-pnpm-store- - v1-${{ runner.os }}- - - - name: Install project dependencies - run: pnpm install - - - name: Store Playwright's Version - run: | - PLAYWRIGHT_VERSION=$(npx playwright --version | sed 's/Version //') - echo "Playwright's Version: $PLAYWRIGHT_VERSION" - echo "PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION" >> $GITHUB_ENV - - - name: Cache Playwright Browsers for Playwright's Version - id: cache-playwright-browsers - uses: actions/cache@v4 - with: - path: ~/.cache/ms-playwright - key: playwright-browsers-${{ env.PLAYWRIGHT_VERSION }} - - - name: Install Playwright Browsers - if: steps.cache-playwright-browsers.outputs.cache-hit != 'true' - run: npx playwright install --with-deps - - - name: Run end-to-end tests - run: cd packages/@sanity/portable-text-editor && npx jest --config=e2e-tests/e2e.config.cjs --silent --shard=${{ matrix.shardIndex}}/${{ matrix.shardTotal }} diff --git a/dev/aliases.cjs b/dev/aliases.cjs index 6d7fd230966..fce9af5ed2d 100644 --- a/dev/aliases.cjs +++ b/dev/aliases.cjs @@ -20,7 +20,6 @@ const devAliases = { '@sanity/diff': './packages/@sanity/diff/src', '@sanity/cli': './packages/@sanity/cli/src', '@sanity/mutator': './packages/@sanity/mutator/src', - '@sanity/portable-text-editor': './packages/@sanity/portable-text-editor/src', '@sanity/schema': './packages/@sanity/schema/src/_exports', '@sanity/migrate': './packages/@sanity/migrate/src/_exports', '@sanity/types': './packages/@sanity/types/src', diff --git a/dev/test-next-studio/next.config.mjs b/dev/test-next-studio/next.config.mjs index 7f33eed88b8..9c8bd47ff68 100644 --- a/dev/test-next-studio/next.config.mjs +++ b/dev/test-next-studio/next.config.mjs @@ -24,7 +24,6 @@ const config = { '@sanity/diff', '@sanity/migrate', '@sanity/mutator', - '@sanity/portable-text-editor', '@sanity/schema', '@sanity/types', '@sanity/util', @@ -40,9 +39,6 @@ const config = { '@sanity/diff': requireResolve('../../packages/@sanity/diff/src/index.ts'), '@sanity/cli': requireResolve('../../packages/@sanity/cli/src/index.ts'), '@sanity/mutator': requireResolve('../../packages/@sanity/mutator/src/index.ts'), - '@sanity/portable-text-editor': requireResolve( - '../../packages/@sanity/portable-text-editor/src/index.ts', - ), '@sanity/schema/_internal': requireResolve( '../../packages/@sanity/schema/src/_exports/_internal.ts', ), @@ -90,7 +86,6 @@ const config = { '@sanity/diff': '@sanity/diff/src/index.ts', '@sanity/cli': '@sanity/cli/src/index.ts', '@sanity/mutator': '@sanity/mutator/src/index.ts', - '@sanity/portable-text-editor': '@sanity/portable-text-editor/src/index.ts', '@sanity/schema/_internal': '@sanity/schema/src/_exports/_internal.ts', '@sanity/schema': '@sanity/schema/src/_exports/index.ts', '@sanity/migrate': '@sanity/migrate/src/_exports/index.ts', diff --git a/dev/test-studio/package.json b/dev/test-studio/package.json index 5556bcb268c..dc8f754d9df 100644 --- a/dev/test-studio/package.json +++ b/dev/test-studio/package.json @@ -16,6 +16,7 @@ "workshop:dev": "node -r esbuild-register scripts/workshop/dev.ts" }, "dependencies": { + "@portabletext/editor": "^1.0.7", "@portabletext/react": "^3.0.0", "@react-three/cannon": "^6.5.2", "@react-three/drei": "^9.80.1", @@ -34,7 +35,6 @@ "@sanity/locale-sv-se": "^1.0.1", "@sanity/logos": "^2.1.2", "@sanity/migrate": "workspace:*", - "@sanity/portable-text-editor": "workspace:*", "@sanity/preview-url-secret": "^1.6.1", "@sanity/react-loader": "^1.8.3", "@sanity/tsdoc": "1.0.72", diff --git a/dev/test-studio/schema/standard/portableText/customMarkers/CustomContentInput.tsx b/dev/test-studio/schema/standard/portableText/customMarkers/CustomContentInput.tsx index f5be2ffa58c..c3f0be63278 100644 --- a/dev/test-studio/schema/standard/portableText/customMarkers/CustomContentInput.tsx +++ b/dev/test-studio/schema/standard/portableText/customMarkers/CustomContentInput.tsx @@ -1,5 +1,5 @@ +import {type OnPasteFn, type PortableTextBlock} from '@portabletext/editor' import {htmlToBlocks} from '@sanity/block-tools' -import {type OnPasteFn, type PortableTextBlock} from '@sanity/portable-text-editor' import {useCallback, useMemo} from 'react' import {PortableTextInput, type PortableTextInputProps, type PortableTextMarker} from 'sanity' diff --git a/dev/test-studio/schema/standard/portableText/customMarkers/blockActions.tsx b/dev/test-studio/schema/standard/portableText/customMarkers/blockActions.tsx index 99e5bdc6f4e..d86756154ec 100644 --- a/dev/test-studio/schema/standard/portableText/customMarkers/blockActions.tsx +++ b/dev/test-studio/schema/standard/portableText/customMarkers/blockActions.tsx @@ -1,5 +1,5 @@ +import {type PortableTextBlock} from '@portabletext/editor' import {CommentIcon} from '@sanity/icons' -import {type PortableTextBlock} from '@sanity/portable-text-editor' import {Box, Button, Popover, Stack, Text, TextArea} from '@sanity/ui' import {type ChangeEvent, useCallback, useState} from 'react' import {type RenderBlockActionsCallback} from 'sanity' diff --git a/dev/tsconfig.dev.json b/dev/tsconfig.dev.json index a0dc008a8dd..a63fa767108 100644 --- a/dev/tsconfig.dev.json +++ b/dev/tsconfig.dev.json @@ -10,7 +10,6 @@ "@sanity/cli": ["./packages/@sanity/cli/src/index.ts"], "@sanity/codegen": ["./packages/@sanity/codegen/src/_exports/index.ts"], "@sanity/mutator": ["./packages/@sanity/mutator/src/index.ts"], - "@sanity/portable-text-editor": ["./packages/@sanity/portable-text-editor/src/index.ts"], "@sanity/schema/*": ["./packages/@sanity/schema/src/_exports/*"], "@sanity/schema": ["./packages/@sanity/schema/src/_exports/index.ts"], "@sanity/migrate": ["./packages/@sanity/migrate/src/_exports/index.ts"], diff --git a/examples/tsconfig.json b/examples/tsconfig.json index a0dc008a8dd..a63fa767108 100644 --- a/examples/tsconfig.json +++ b/examples/tsconfig.json @@ -10,7 +10,6 @@ "@sanity/cli": ["./packages/@sanity/cli/src/index.ts"], "@sanity/codegen": ["./packages/@sanity/codegen/src/_exports/index.ts"], "@sanity/mutator": ["./packages/@sanity/mutator/src/index.ts"], - "@sanity/portable-text-editor": ["./packages/@sanity/portable-text-editor/src/index.ts"], "@sanity/schema/*": ["./packages/@sanity/schema/src/_exports/*"], "@sanity/schema": ["./packages/@sanity/schema/src/_exports/index.ts"], "@sanity/migrate": ["./packages/@sanity/migrate/src/_exports/index.ts"], diff --git a/packages/@repo/test-exports/.depcheckrc.json b/packages/@repo/test-exports/.depcheckrc.json index e13737c7d56..7be5e5048b1 100644 --- a/packages/@repo/test-exports/.depcheckrc.json +++ b/packages/@repo/test-exports/.depcheckrc.json @@ -7,7 +7,6 @@ "@sanity/diff", "@sanity/migrate", "@sanity/mutator", - "@sanity/portable-text-editor", "@sanity/schema", "@sanity/types", "@sanity/util", diff --git a/packages/@repo/test-exports/package.json b/packages/@repo/test-exports/package.json index 01c385b55b8..d83d48d1e84 100644 --- a/packages/@repo/test-exports/package.json +++ b/packages/@repo/test-exports/package.json @@ -16,7 +16,6 @@ "@sanity/diff": "workspace:*", "@sanity/migrate": "workspace:*", "@sanity/mutator": "workspace:*", - "@sanity/portable-text-editor": "workspace:*", "@sanity/schema": "workspace:*", "@sanity/types": "workspace:*", "@sanity/util": "workspace:*", diff --git a/packages/@sanity/portable-text-editor/.depcheckrc.json b/packages/@sanity/portable-text-editor/.depcheckrc.json deleted file mode 100644 index 3b754900443..00000000000 --- a/packages/@sanity/portable-text-editor/.depcheckrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "ignores": ["@repo/tsconfig", "@sanity/pkg-utils", "ws", "@types/jest"] -} diff --git a/packages/@sanity/portable-text-editor/.eslintrc.cjs b/packages/@sanity/portable-text-editor/.eslintrc.cjs deleted file mode 100644 index 99fd6c69224..00000000000 --- a/packages/@sanity/portable-text-editor/.eslintrc.cjs +++ /dev/null @@ -1,11 +0,0 @@ -'use strict' - -const path = require('path') - -const ROOT_PATH = path.resolve(__dirname, '../../..') - -module.exports = { - rules: { - 'import/no-extraneous-dependencies': ['error', {packageDir: [ROOT_PATH, __dirname]}], - }, -} diff --git a/packages/@sanity/portable-text-editor/.gitignore b/packages/@sanity/portable-text-editor/.gitignore deleted file mode 100644 index 626befc941e..00000000000 --- a/packages/@sanity/portable-text-editor/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -# Logs -/logs -*.log - -# Coverage directory used by tools like istanbul -/coverage - -# Dependency directories -/node_modules - -# Compiled code -/lib -/dist diff --git a/packages/@sanity/portable-text-editor/LICENSE b/packages/@sanity/portable-text-editor/LICENSE deleted file mode 100644 index c5f080fd508..00000000000 --- a/packages/@sanity/portable-text-editor/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2016 - 2024 Sanity.io - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/packages/@sanity/portable-text-editor/README.md b/packages/@sanity/portable-text-editor/README.md deleted file mode 100644 index bd3c223786b..00000000000 --- a/packages/@sanity/portable-text-editor/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# ⚠ This package is deprecated ️ - -Please note that this package is deprecated and has been replaced by [@portabletext/editor](https://www.npmjs.com/package/@portabletext/editor). diff --git a/packages/@sanity/portable-text-editor/e2e-tests/__tests__/.eslintrc b/packages/@sanity/portable-text-editor/e2e-tests/__tests__/.eslintrc deleted file mode 100644 index a510f587667..00000000000 --- a/packages/@sanity/portable-text-editor/e2e-tests/__tests__/.eslintrc +++ /dev/null @@ -1,10 +0,0 @@ -{ - "env": { - "browser": false, - "node": true - }, - "rules": { - "import/no-unassigned-import": "off", - "tsdoc/syntax": "off" - } -} diff --git a/packages/@sanity/portable-text-editor/e2e-tests/__tests__/pasting.collaborative.test.ts b/packages/@sanity/portable-text-editor/e2e-tests/__tests__/pasting.collaborative.test.ts deleted file mode 100644 index 6fd6f35a89c..00000000000 --- a/packages/@sanity/portable-text-editor/e2e-tests/__tests__/pasting.collaborative.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -/** @jest-environment ./setup/collaborative.jest.env.ts */ -import '../setup/globals.jest' - -import os from 'node:os' - -import {describe, expect, it} from '@jest/globals' -import {noop} from 'lodash' - -function isMacOs() { - return os.platform() === 'darwin' -} - -// Ideally pasting should be tested in a testing-library test, but I have not found a way to do it natively with testing-lib. -// The problem is to get permission to write to the host clipboard. -// We can do it in these test's though (as we can override browser permissions through packages/@sanity/portable-text-editor/test/setup/collaborative.jest.env.ts) -describe('pasting', () => { - // 24/04/2023 - Something happened with chromium-1055 and MacOS that doesn't allow the pasting keyboard shortcut to take effect. - // TODO: check up on this when a new Chromium is released for Playwright and remove this exception if it is working again. - if (isMacOs()) { - it('skips these tests on macOS', noop) - return - } - it('can paste into an empty editor', async () => { - const [editorA] = await getEditors() - await editorA.paste('Yo!') - const valueA = await editorA.getValue() - expect(valueA).toMatchObject([ - { - // _key: 'A-4', // Keys seem to vary between platforms when pasting. Linux have an additional call to the key-generator, not sure why. Only happens when pasting. - _type: 'block', - children: [{_type: 'span', marks: [], text: 'Yo!'}], // _key is random here (from @sanity/block-tools) and is left out. - markDefs: [], - style: 'normal', - }, - ]) - }) - - it('can paste into an populated editor', async () => { - const [editorA, editorB] = await getEditors() - await editorB.insertText('Hey!') - await editorA.paste('Yo!') - const valueA = await editorA.getValue() - expect(valueA).toMatchObject([ - { - // _key: 'B-0', // Keys seem to vary between platforms when pasting. Linux have an additional call to the key-generator, not sure why. Only happens when pasting. - _type: 'block', - children: [{_type: 'span', marks: [], text: 'Hey!Yo!'}], // _key is random here (from @sanity/block-tools) and is left out. - markDefs: [], - style: 'normal', - }, - ]) - }) - - it('can paste empty lines from clipboard without duplicating keys', async () => { - const [editorA] = await getEditors() - await editorA.paste('\n\n', 'text/plain') - const data = `
-
- Lala
Lala
` - await editorA.paste(data, 'text/html') - const valueA = await editorA.getValue() - expect(valueA).toMatchObject([ - { - // _key: 'A-4', // Keys seem to vary between platforms when pasting. Linux have an additional call to the key-generator, not sure why. Only happens when pasting. - children: [ - { - _type: 'span', - text: '', - marks: [], - }, - ], - markDefs: [], - _type: 'block', - style: 'normal', - }, - { - // _key: 'A-6', - children: [ - { - _type: 'span', - marks: [], - text: 'LalaLala', - }, - ], - markDefs: [], - _type: 'block', - style: 'normal', - }, - ]) - }) -}) diff --git a/packages/@sanity/portable-text-editor/e2e-tests/__tests__/selectionAdjustment.collaborative.test.ts b/packages/@sanity/portable-text-editor/e2e-tests/__tests__/selectionAdjustment.collaborative.test.ts deleted file mode 100644 index 48a61782569..00000000000 --- a/packages/@sanity/portable-text-editor/e2e-tests/__tests__/selectionAdjustment.collaborative.test.ts +++ /dev/null @@ -1,571 +0,0 @@ -/** @jest-environment ./setup/collaborative.jest.env.ts */ -import '../setup/globals.jest' - -import {describe, expect, it} from '@jest/globals' - -describe('selection adjustment', () => { - describe('insert and unset blocks', () => { - it('will keep A on same line if B insert above', async () => { - await setDocumentValue([ - { - _key: 'someKey', - _type: 'block', - markDefs: [], - style: 'normal', - children: [{_key: 'anotherKey', _type: 'span', text: 'Hello', marks: []}], - }, - ]) - const expectedSelectionA = { - anchor: {path: [{_key: 'someKey'}, 'children', {_key: 'anotherKey'}], offset: 2}, - focus: {path: [{_key: 'someKey'}, 'children', {_key: 'anotherKey'}], offset: 2}, - backward: false, - } - const [editorA, editorB] = await getEditors() - await editorA.pressKey('ArrowRight', 2) - let selectionA = await editorA.getSelection() - expect(selectionA).toEqual(expectedSelectionA) - await editorB.pressKey('Enter') - expect(await editorA.getValue()).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "B-7", - "_type": "block", - "children": Array [ - Object { - "_key": "B-6", - "_type": "span", - "marks": Array [], - "text": "", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - Object { - "_key": "someKey", - "_type": "block", - "children": Array [ - Object { - "_key": "anotherKey", - "_type": "span", - "marks": Array [], - "text": "Hello", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - selectionA = await editorA.getSelection() - expect(selectionA).toMatchInlineSnapshot(` - Object { - "anchor": Object { - "offset": 2, - "path": Array [ - Object { - "_key": "someKey", - }, - "children", - Object { - "_key": "anotherKey", - }, - ], - }, - "backward": false, - "focus": Object { - "offset": 2, - "path": Array [ - Object { - "_key": "someKey", - }, - "children", - Object { - "_key": "anotherKey", - }, - ], - }, - } - `) - }) - - it('will keep A on same line if B delete a line above', async () => { - await setDocumentValue([ - { - _key: 'someKey1', - _type: 'block', - markDefs: [], - style: 'normal', - children: [{_key: 'anotherKey1', _type: 'span', text: 'One', marks: []}], - }, - { - _key: 'someKey2', - _type: 'block', - markDefs: [], - style: 'normal', - children: [{_key: 'anotherKey2', _type: 'span', text: 'Two', marks: []}], - }, - { - _key: 'someKey3', - _type: 'block', - markDefs: [], - style: 'normal', - children: [{_key: 'anotherKey3', _type: 'span', text: 'Three', marks: []}], - }, - ]) - const expectedSelection = { - anchor: {path: [{_key: 'someKey2'}, 'children', {_key: 'anotherKey2'}], offset: 2}, - focus: {path: [{_key: 'someKey2'}, 'children', {_key: 'anotherKey2'}], offset: 2}, - backward: false, - } - const [editorA, editorB] = await getEditors() - await editorA.setSelection(expectedSelection) - expect(await editorA.getSelection()).toEqual(expectedSelection) - await editorB.setSelection({ - anchor: {path: [{_key: 'someKey1'}, 'children', {_key: 'anotherKey1'}], offset: 3}, - focus: {path: [{_key: 'someKey1'}, 'children', {_key: 'anotherKey1'}], offset: 3}, - }) - await editorB.pressKey('Backspace', 3) - await editorB.pressKey('Delete') - const valueB = await editorB.getValue() - expect(valueB).toEqual([ - { - _key: 'someKey2', - _type: 'block', - markDefs: [], - style: 'normal', - children: [ - { - _key: 'anotherKey2', - _type: 'span', - text: 'Two', - marks: [], - }, - ], - }, - { - _key: 'someKey3', - _type: 'block', - markDefs: [], - style: 'normal', - children: [ - { - _key: 'anotherKey3', - _type: 'span', - text: 'Three', - marks: [], - }, - ], - }, - ]) - expect(await editorA.getSelection()).toEqual(expectedSelection) - }) - it('will keep A on same line if B backspace-deletes an empty line above', async () => { - await setDocumentValue([ - { - _key: 'someKey1', - _type: 'block', - markDefs: [], - style: 'normal', - children: [{_key: 'anotherKey1', _type: 'span', text: '', marks: []}], - }, - { - _key: 'someKey2', - _type: 'block', - markDefs: [], - style: 'normal', - children: [{_key: 'anotherKey2', _type: 'span', text: '', marks: []}], - }, - { - _key: 'someKey3', - _type: 'block', - markDefs: [], - style: 'normal', - children: [{_key: 'anotherKey3', _type: 'span', text: '', marks: []}], - }, - { - _key: 'someKey4', - _type: 'block', - markDefs: [], - style: 'normal', - children: [{_key: 'anotherKey4', _type: 'span', text: '', marks: []}], - }, - { - _key: 'someKey5', - _type: 'block', - markDefs: [], - style: 'normal', - children: [{_key: 'anotherKey5', _type: 'span', text: 'Three', marks: []}], - }, - ]) - const expectedSelection = { - anchor: {path: [{_key: 'someKey5'}, 'children', {_key: 'anotherKey5'}], offset: 5}, - focus: {path: [{_key: 'someKey5'}, 'children', {_key: 'anotherKey5'}], offset: 5}, - backward: false, - } - const [editorA, editorB] = await getEditors() - await editorA.setSelection(expectedSelection) - expect(await editorA.getSelection()).toEqual(expectedSelection) - await editorB.setSelection({ - anchor: {path: [{_key: 'someKey4'}, 'children', {_key: 'anotherKey4'}], offset: 0}, - focus: {path: [{_key: 'someKey4'}, 'children', {_key: 'anotherKey4'}], offset: 0}, - }) - await editorB.pressKey('Backspace') - await editorB.pressKey('Backspace') - await editorB.pressKey('Backspace') - const valueA = await editorA.getValue() - const valueB = await editorB.getValue() - expect(valueB).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "someKey4", - "_type": "block", - "children": Array [ - Object { - "_key": "anotherKey4", - "_type": "span", - "marks": Array [], - "text": "", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - Object { - "_key": "someKey5", - "_type": "block", - "children": Array [ - Object { - "_key": "anotherKey5", - "_type": "span", - "marks": Array [], - "text": "Three", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - expect(valueA).toEqual(valueB) - expect(await editorA.getSelection()).toEqual(expectedSelection) - }) - it('will keep A on same line if B inserts a line above', async () => { - await setDocumentValue([ - { - _key: 'someKey2', - _type: 'block', - markDefs: [], - style: 'normal', - children: [{_key: 'anotherKey2', _type: 'span', text: '', marks: []}], - }, - { - _key: 'someKey3', - _type: 'block', - markDefs: [], - style: 'normal', - children: [{_key: 'anotherKey3', _type: 'span', text: 'Three', marks: []}], - }, - ]) - const expectedSelection = { - anchor: {path: [{_key: 'someKey3'}, 'children', {_key: 'anotherKey3'}], offset: 0}, - focus: {path: [{_key: 'someKey3'}, 'children', {_key: 'anotherKey3'}], offset: 0}, - backward: false, - } - const [editorA, editorB] = await getEditors() - await editorA.setSelection(expectedSelection) - expect(await editorA.getSelection()).toEqual(expectedSelection) - await editorB.setSelection({ - anchor: {path: [{_key: 'someKey2'}, 'children', {_key: 'anotherKey2'}], offset: 0}, - focus: {path: [{_key: 'someKey2'}, 'children', {_key: 'anotherKey2'}], offset: 0}, - }) - await editorB.pressKey('Enter') - const valueA = await editorA.getValue() - const valueB = await editorB.getValue() - expect(valueB).toEqual([ - { - _key: 'someKey2', - _type: 'block', - markDefs: [], - style: 'normal', - children: [ - { - _key: 'anotherKey2', - _type: 'span', - text: '', - marks: [], - }, - ], - }, - { - _key: 'B-6', - _type: 'block', - markDefs: [], - style: 'normal', - children: [ - { - _key: 'B-5', - _type: 'span', - text: '', - marks: [], - }, - ], - }, - { - _key: 'someKey3', - _type: 'block', - markDefs: [], - style: 'normal', - children: [ - { - _key: 'anotherKey3', - _type: 'span', - text: 'Three', - marks: [], - }, - ], - }, - ]) - expect(valueA).toEqual(valueB) - expect(await editorA.getSelection()).toEqual(expectedSelection) - }) - }) - - describe('when merging text', () => { - it("will keep A on same word if B merges A's line into the above line", async () => { - await setDocumentValue([ - { - _key: 'someKey5', - _type: 'block', - markDefs: [], - style: 'normal', - children: [{_key: 'anotherKey5', _type: 'span', text: '1', marks: []}], - }, - { - _key: 'someKey6', - _type: 'block', - markDefs: [], - style: 'normal', - children: [{_key: 'anotherKey6', _type: 'span', text: '22', marks: []}], - }, - { - _key: 'someKey7', - _type: 'block', - markDefs: [], - style: 'normal', - children: [{_key: 'anotherKey7', _type: 'span', text: '333', marks: []}], - }, - ]) - const expectedSelection = { - anchor: {path: [{_key: 'someKey6'}, 'children', {_key: 'anotherKey6'}], offset: 2}, - focus: {path: [{_key: 'someKey6'}, 'children', {_key: 'anotherKey6'}], offset: 2}, - backward: false, - } - const [editorA, editorB] = await getEditors() - await editorA.setSelection(expectedSelection) - expect(await editorA.getSelection()).toEqual(expectedSelection) - await editorB.setSelection({ - anchor: {path: [{_key: 'someKey6'}, 'children', {_key: 'anotherKey6'}], offset: 0}, - focus: {path: [{_key: 'someKey6'}, 'children', {_key: 'anotherKey6'}], offset: 0}, - }) - await editorB.pressKey('Backspace') - const valueB = await editorB.getValue() - expect(valueB).toEqual([ - { - _key: 'someKey5', - _type: 'block', - markDefs: [], - style: 'normal', - children: [ - { - _key: 'anotherKey5', - _type: 'span', - text: '122', - marks: [], - }, - ], - }, - { - _key: 'someKey7', - _type: 'block', - markDefs: [], - style: 'normal', - children: [ - { - _key: 'anotherKey7', - _type: 'span', - text: '333', - marks: [], - }, - ], - }, - ]) - const valueA = await editorA.getValue() - expect(valueA).toEqual(valueB) - expect(await editorA.getSelection()).toMatchInlineSnapshot(` - Object { - "anchor": Object { - "offset": 0, - "path": Array [ - Object { - "_key": "someKey7", - }, - "children", - Object { - "_key": "anotherKey7", - }, - ], - }, - "backward": false, - "focus": Object { - "offset": 0, - "path": Array [ - Object { - "_key": "someKey7", - }, - "children", - Object { - "_key": "anotherKey7", - }, - ], - }, - } - `) - }) - }) - - it('will keep A on same word if B merges marks within that line', async () => { - await setDocumentValue([ - { - _key: 'someKey', - _type: 'block', - markDefs: [], - style: 'normal', - children: [ - {_key: 'anotherKey1', _type: 'span', text: '1 ', marks: []}, - {_key: 'anotherKey2', _type: 'span', text: '22', marks: ['strong']}, - {_key: 'anotherKey3', _type: 'span', text: ' 333', marks: []}, - ], - }, - ]) - const expectedSelectionA = { - anchor: {path: [{_key: 'someKey'}, 'children', {_key: 'anotherKey3'}], offset: 1}, - focus: {path: [{_key: 'someKey'}, 'children', {_key: 'anotherKey3'}], offset: 1}, - backward: false, - } - const [editorA, editorB] = await getEditors() - await editorA.setSelection(expectedSelectionA) - expect(await editorA.getSelection()).toEqual(expectedSelectionA) - const expectedSelectionB = { - anchor: {path: [{_key: 'someKey'}, 'children', {_key: 'anotherKey2'}], offset: 0}, - focus: {path: [{_key: 'someKey'}, 'children', {_key: 'anotherKey2'}], offset: 2}, - } - await editorB.setSelection(expectedSelectionB) - expect(await editorB.getSelection()).toEqual(expectedSelectionB) - await editorB.toggleMark('b') - const valueB = await editorB.getValue() - expect(valueB).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "someKey", - "_type": "block", - "children": Array [ - Object { - "_key": "anotherKey1", - "_type": "span", - "marks": Array [], - "text": "1 22 333", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - const valueA = await editorA.getValue() - expect(valueA).toEqual(valueB) - expect(await editorA.getSelection()).toEqual({ - anchor: {path: [{_key: 'someKey'}, 'children', {_key: 'anotherKey1'}], offset: 8}, - focus: {path: [{_key: 'someKey'}, 'children', {_key: 'anotherKey1'}], offset: 8}, - backward: false, - }) - }) - - it('will keep A on same selection if B toggles marks on another block', async () => { - await setDocumentValue([ - { - _key: 'someKey1', - _type: 'block', - markDefs: [], - style: 'normal', - children: [{_key: 'anotherKey1', _type: 'span', text: '1', marks: ['strong']}], - }, - { - _key: 'someKey2', - _type: 'block', - markDefs: [], - style: 'normal', - children: [{_key: 'anotherKey2', _type: 'span', text: '2', marks: []}], - }, - ]) - const expectedSelectionA = { - anchor: {path: [{_key: 'someKey1'}, 'children', {_key: 'anotherKey1'}], offset: 0}, - focus: {path: [{_key: 'someKey1'}, 'children', {_key: 'anotherKey1'}], offset: 1}, - } - const expectedSelectionB = { - anchor: {path: [{_key: 'someKey2'}, 'children', {_key: 'anotherKey2'}], offset: 0}, - focus: {path: [{_key: 'someKey2'}, 'children', {_key: 'anotherKey2'}], offset: 1}, - } - const [editorA, editorB] = await getEditors() - await editorA.setSelection(expectedSelectionA) - await editorB.setSelection(expectedSelectionB) - expect(await editorA.getSelection()).toEqual(expectedSelectionA) - expect(await editorB.getSelection()).toEqual(expectedSelectionB) - await editorA.toggleMark('b') - const valueB = await editorB.getValue() - expect(valueB).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "someKey1", - "_type": "block", - "children": Array [ - Object { - "_key": "anotherKey1", - "_type": "span", - "marks": Array [], - "text": "1", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - Object { - "_key": "someKey2", - "_type": "block", - "children": Array [ - Object { - "_key": "anotherKey2", - "_type": "span", - "marks": Array [], - "text": "2", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - const valueA = await editorA.getValue() - expect(valueA).toEqual(valueB) - expect(await editorA.getSelection()).toEqual({ - anchor: {path: [{_key: 'someKey1'}, 'children', {_key: 'anotherKey1'}], offset: 0}, - focus: {path: [{_key: 'someKey1'}, 'children', {_key: 'anotherKey1'}], offset: 1}, - backward: false, - }) - expect(await editorB.getSelection()).toEqual({ - anchor: {path: [{_key: 'someKey2'}, 'children', {_key: 'anotherKey2'}], offset: 0}, - focus: {path: [{_key: 'someKey2'}, 'children', {_key: 'anotherKey2'}], offset: 1}, - backward: false, - }) - }) -}) diff --git a/packages/@sanity/portable-text-editor/e2e-tests/__tests__/undoRedo.collborative.test.ts b/packages/@sanity/portable-text-editor/e2e-tests/__tests__/undoRedo.collborative.test.ts deleted file mode 100644 index e4dc49eaacf..00000000000 --- a/packages/@sanity/portable-text-editor/e2e-tests/__tests__/undoRedo.collborative.test.ts +++ /dev/null @@ -1,1412 +0,0 @@ -/** @jest-environment ./setup/collaborative.jest.env.ts */ -import '../setup/globals.jest' - -import {describe, expect, it} from '@jest/globals' -import {toPlainText} from '@portabletext/toolkit' -import {type PortableTextBlock} from '@sanity/types' - -const initialValue: PortableTextBlock[] = [ - { - _key: 'randomKey0', - _type: 'block', - markDefs: [], - style: 'normal', - children: [ - { - _key: 'randomKey1', - _type: 'span', - text: 'Hello world', - marks: [], - }, - ], - }, -] - -function getInitialValue(text = 'Hello world'): PortableTextBlock[] { - return [ - { - _key: 'blockA', - _type: 'block', - markDefs: [], - style: 'normal', - children: [ - { - _key: 'spanA', - _type: 'span', - text, - marks: [], - }, - ], - }, - ] -} - -describe('undo/redo', () => { - it("will let editor A undo all changes after B wrote something in between A's changes", async () => { - await setDocumentValue(initialValue) - const [editorA, editorB] = await getEditors() - const desiredSelectionA = { - anchor: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 18}, - focus: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 18}, - } - await editorA.setSelection(desiredSelectionA) - await editorA.pressKey('Backspace') - await editorB.setSelection({ - anchor: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 11}, - focus: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 11}, - }) - await editorB.insertText(' there!') - let valA = await editorA.getValue() - let valB = await editorB.getValue() - expect(valA).toEqual(valB) - expect(valB).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "randomKey0", - "_type": "block", - "children": Array [ - Object { - "_key": "randomKey1", - "_type": "span", - "marks": Array [], - "text": "Hello worl there!", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - await editorA.undo() - valA = await editorA.getValue() - valB = await editorB.getValue() - expect(valA).toEqual(valB) - expect(valB).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "randomKey0", - "_type": "block", - "children": Array [ - Object { - "_key": "randomKey1", - "_type": "span", - "marks": Array [], - "text": "Hello world there!", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - }) - - it('will let editor A undo their change after B did an unrelated change (multi-line block)', async () => { - const initialText = 'First paragraph\n\nSecond paragraph!' - await setDocumentValue(getInitialValue(initialText)) - const [editorA, editorB] = await getEditors() - - // Sanity-test for initial value - expect(toPlainText((await editorA.getValue()) || [])).toBe(initialText) - - // Editor A sets selection at end of the second paragraph (after "!"), and adds a question mark - const charOffset = initialText.indexOf('!') + 1 - const desiredSelectionA = { - anchor: {path: [{_key: 'blockA'}, 'children', {_key: 'spanA'}], offset: charOffset}, - focus: {path: [{_key: 'blockA'}, 'children', {_key: 'spanA'}], offset: charOffset}, - } - await editorA.setSelection(desiredSelectionA) - await editorA.insertText('?') - - // Sanity-test for edit - const expectedEditedText = initialText.replace(/!/g, '!?') - expect(toPlainText((await editorA.getValue()) || [])).toBe(expectedEditedText) - - // Editor B adds a new paragraph (_within same block_) to the start of the editor - const newPrefix = 'Welcome.\n\n' - await editorB.setSelection({ - anchor: {path: [{_key: 'blockA'}, 'children', {_key: 'spanA'}], offset: 0}, - focus: {path: [{_key: 'blockA'}, 'children', {_key: 'spanA'}], offset: 0}, - }) - await editorB.insertText(newPrefix) - - // Editors should be in sync - let valA = await editorA.getValue() - let valB = await editorB.getValue() - expect(valA).toEqual(valB) - - // Should have the edit from editor A, and the new paragraph from editor B - expect(toPlainText(valA || [])).toBe(newPrefix + expectedEditedText) - - // Editor A undos their edit (`!?` => `!`) - await editorA.undo() - - // Editors should still be in sync - valA = await editorA.getValue() - valB = await editorB.getValue() - expect(valA).toEqual(valB) - - // Sanity-check that the value is still only a single block, not multiple - expect(valA || []).toHaveLength(1) - - // Shape of editor should be the expected prefix + initial value - expect(toPlainText(valA || [])).toBe(newPrefix + initialText) - }) - - it("will let editor A undo all changes after B pressed Enter in between A's changes", async () => { - await setDocumentValue(initialValue) - const [editorA, editorB] = await getEditors() - const desiredSelectionA = { - anchor: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 18}, - focus: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 18}, - } - await editorA.setSelection(desiredSelectionA) - await editorA.pressKey('Enter') - await editorA.insertText('Hey!') - await editorB.setSelection({ - anchor: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 11}, - focus: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 11}, - }) - await editorB.insertText(' there!') - let valA = await editorA.getValue() - let valB = await editorB.getValue() - expect(valA).toEqual(valB) - expect(valB).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "randomKey0", - "_type": "block", - "children": Array [ - Object { - "_key": "randomKey1", - "_type": "span", - "marks": Array [], - "text": "Hello world there!", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - Object { - "_key": "A-6", - "_type": "block", - "children": Array [ - Object { - "_key": "A-5", - "_type": "span", - "marks": Array [], - "text": "Hey!", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - await editorA.undo() - await editorB.undo() - valA = await editorA.getValue() - valB = await editorB.getValue() - expect(valA).toEqual(valB) - expect(valB).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "randomKey0", - "_type": "block", - "children": Array [ - Object { - "_key": "randomKey1", - "_type": "span", - "marks": Array [], - "text": "Hello world", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - Object { - "_key": "A-6", - "_type": "block", - "children": Array [ - Object { - "_key": "A-5", - "_type": "span", - "marks": Array [], - "text": "", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - }) - - it('will undo respective changes in same text node correctly', async () => { - const val: PortableTextBlock[] = [ - { - _key: 'randomKey0', - _type: 'block', - markDefs: [], - style: 'normal', - children: [ - { - _key: 'randomKey1', - _type: 'span', - text: 'Hello world there!', - marks: [], - }, - ], - }, - ] - await setDocumentValue(val) - const [editorA, editorB] = await getEditors() - const startSelectionA = { - anchor: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 5}, - focus: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 5}, - backward: false, - } - await editorA.setSelection(startSelectionA) - const startSelectionB = { - anchor: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 11}, - focus: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 11}, - backward: false, - } - await editorB.setSelection(startSelectionB) - await editorA.insertText('123') - await editorB.insertText('ABC') - let valA = await editorA.getValue() - let valB = await editorB.getValue() - expect(valA).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "randomKey0", - "_type": "block", - "children": Array [ - Object { - "_key": "randomKey1", - "_type": "span", - "marks": Array [], - "text": "Hello123 worldABC there!", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - expect(valA).toEqual(valB) - await editorA.undo() - valA = await editorA.getValue() - valB = await editorB.getValue() - expect(valA).toEqual(valB) - expect(valB).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "randomKey0", - "_type": "block", - "children": Array [ - Object { - "_key": "randomKey1", - "_type": "span", - "marks": Array [], - "text": "Hello worldABC there!", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - await editorB.undo() - valA = await editorA.getValue() - valB = await editorB.getValue() - expect(valA).toEqual(valB) - expect(valB).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "randomKey0", - "_type": "block", - "children": Array [ - Object { - "_key": "randomKey1", - "_type": "span", - "marks": Array [], - "text": "Hello world there!", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - const selectionA = await editorA.getSelection() - expect(selectionA).toEqual(startSelectionA) - const selectionB = await editorB.getSelection() - expect(selectionB).toEqual(startSelectionB) - }) - - it("will undo correctly for editorB when editor A writes something before editor B's edits", async () => { - const val: PortableTextBlock[] = [ - { - _key: 'randomKey0', - _type: 'block', - markDefs: [], - style: 'normal', - children: [ - { - _key: 'randomKey1', - _type: 'span', - text: 'Hello world there!', - marks: [], - }, - ], - }, - ] - await setDocumentValue(val) - const [editorA, editorB] = await getEditors() - const startSelectionA = { - anchor: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 5}, - focus: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 5}, - backward: false, - } - await editorA.setSelection(startSelectionA) - const startSelectionB = { - anchor: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 18}, - focus: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 18}, - backward: false, - } - await editorB.setSelection(startSelectionB) - await editorB.pressKey('Backspace') - await editorA.insertText('123') - let valA = await editorA.getValue() - let valB = await editorB.getValue() - expect(valA).toEqual(valB) - expect(valA).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "randomKey0", - "_type": "block", - "children": Array [ - Object { - "_key": "randomKey1", - "_type": "span", - "marks": Array [], - "text": "Hello123 world there", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - await editorB.undo() - valA = await editorA.getValue() - valB = await editorB.getValue() - expect(valA).toEqual(valB) - expect(valA).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "randomKey0", - "_type": "block", - "children": Array [ - Object { - "_key": "randomKey1", - "_type": "span", - "marks": Array [], - "text": "Hello123 world there!", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - await editorA.undo() - valA = await editorA.getValue() - valB = await editorB.getValue() - expect(valA).toEqual(valB) - expect(valA).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "randomKey0", - "_type": "block", - "children": Array [ - Object { - "_key": "randomKey1", - "_type": "span", - "marks": Array [], - "text": "Hello world there!", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - }) - - it("will let editor A undo all changes after B pressed Enter and wrote something in between A's changes", async () => { - await setDocumentValue(initialValue) - const [editorA, editorB] = await getEditors() - const desiredSelectionA = { - anchor: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 18}, - focus: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 18}, - backward: false, - } - await editorA.setSelection(desiredSelectionA) - await editorA.pressKey('Enter') - await editorA.insertText('Hey!') - await editorB.setSelection({ - anchor: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 11}, - focus: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 11}, - }) - await editorB.insertText(' there!') - let valA = await editorA.getValue() - let valB = await editorB.getValue() - expect(valA).toEqual(valB) - expect(valB).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "randomKey0", - "_type": "block", - "children": Array [ - Object { - "_key": "randomKey1", - "_type": "span", - "marks": Array [], - "text": "Hello world there!", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - Object { - "_key": "A-6", - "_type": "block", - "children": Array [ - Object { - "_key": "A-5", - "_type": "span", - "marks": Array [], - "text": "Hey!", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - await editorA.undo() - await editorA.undo() - valA = await editorA.getValue() - valB = await editorB.getValue() - expect(valA).toEqual(valB) - expect(valB).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "randomKey0", - "_type": "block", - "children": Array [ - Object { - "_key": "randomKey1", - "_type": "span", - "marks": Array [], - "text": "Hello world there!", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - }) - - it('will undo respective changes in same text node correctly when splitting a block', async () => { - const val: PortableTextBlock[] = [ - { - _key: 'randomKey0', - _type: 'block', - markDefs: [], - style: 'normal', - children: [ - { - _key: 'randomKey1', - _type: 'span', - text: 'Hello world there!', - marks: [], - }, - ], - }, - ] - await setDocumentValue(val) - const [editorA, editorB] = await getEditors() - const startSelectionA = { - anchor: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 5}, - focus: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 5}, - backward: false, - } - await editorA.setSelection(startSelectionA) - const startSelectionB = { - anchor: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 11}, - focus: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 11}, - backward: false, - } - await editorB.setSelection(startSelectionB) - await editorA.insertText('123') - await editorB.insertText('ABC') - await editorA.pressKey('Enter') - let valA = await editorA.getValue() - let valB = await editorB.getValue() - expect(valA).toEqual(valB) - expect(valB).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "randomKey0", - "_type": "block", - "children": Array [ - Object { - "_key": "randomKey1", - "_type": "span", - "marks": Array [], - "text": "Hello123", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - Object { - "_key": "A-6", - "_type": "block", - "children": Array [ - Object { - "_key": "A-5", - "_type": "span", - "marks": Array [], - "text": " worldABC there!", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - await editorA.undo() - await editorB.undo() - valA = await editorA.getValue() - valB = await editorB.getValue() - expect(valA).toEqual(valB) - expect(valB).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "randomKey0", - "_type": "block", - "children": Array [ - Object { - "_key": "randomKey1", - "_type": "span", - "marks": Array [], - "text": "Hello123 world there!", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - const selectionA = await editorA.getSelection() - expect(selectionA).toMatchInlineSnapshot(` - Object { - "anchor": Object { - "offset": 8, - "path": Array [ - Object { - "_key": "randomKey0", - }, - "children", - Object { - "_key": "randomKey1", - }, - ], - }, - "backward": false, - "focus": Object { - "offset": 8, - "path": Array [ - Object { - "_key": "randomKey0", - }, - "children", - Object { - "_key": "randomKey1", - }, - ], - }, - } - `) - const selectionB = await editorB.getSelection() - expect(selectionB).toMatchInlineSnapshot(` - Object { - "anchor": Object { - "offset": 14, - "path": Array [ - Object { - "_key": "randomKey0", - }, - "children", - Object { - "_key": "randomKey1", - }, - ], - }, - "backward": false, - "focus": Object { - "offset": 14, - "path": Array [ - Object { - "_key": "randomKey0", - }, - "children", - Object { - "_key": "randomKey1", - }, - ], - }, - } - `) - }) - - it('will undo respective changes in same text node correctly', async () => { - const val: PortableTextBlock[] = [ - { - _key: 'randomKey0', - _type: 'block', - markDefs: [], - style: 'normal', - children: [ - { - _key: 'randomKey1', - _type: 'span', - text: 'Hello world there!', - marks: [], - }, - ], - }, - ] - await setDocumentValue(val) - const [editorA, editorB] = await getEditors() - const startSelectionA = { - anchor: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 5}, - focus: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 5}, - backward: false, - } - await editorA.setSelection(startSelectionA) - const startSelectionB = { - anchor: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 11}, - focus: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 11}, - backward: false, - } - await editorB.setSelection(startSelectionB) - await editorA.pressKey('1') - await editorA.pressKey('2') - await editorA.pressKey('3') - await editorB.pressKey('A') - await editorB.pressKey('B') - await editorB.pressKey('C') - let valA = await editorA.getValue() - let valB = await editorB.getValue() - expect(valA).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "randomKey0", - "_type": "block", - "children": Array [ - Object { - "_key": "randomKey1", - "_type": "span", - "marks": Array [], - "text": "Hello123 worldABC there!", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - expect(valA).toEqual(valB) - await editorA.undo() - valA = await editorA.getValue() - valB = await editorB.getValue() - expect(valA).toEqual(valB) - expect(valB).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "randomKey0", - "_type": "block", - "children": Array [ - Object { - "_key": "randomKey1", - "_type": "span", - "marks": Array [], - "text": "Hello worldABC there!", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - await editorB.undo() - valA = await editorA.getValue() - valB = await editorB.getValue() - expect(valA).toEqual(valB) - expect(valB).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "randomKey0", - "_type": "block", - "children": Array [ - Object { - "_key": "randomKey1", - "_type": "span", - "marks": Array [], - "text": "Hello world there!", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - const selectionA = await editorA.getSelection() - expect(selectionA).toEqual(startSelectionA) - const selectionB = await editorB.getSelection() - expect(selectionB).toEqual(startSelectionB) - await editorA.redo() - valA = await editorA.getValue() - valB = await editorB.getValue() - expect(valA).toEqual(valB) - expect(valB).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "randomKey0", - "_type": "block", - "children": Array [ - Object { - "_key": "randomKey1", - "_type": "span", - "marks": Array [], - "text": "Hello123 world there!", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - await editorB.redo() - valA = await editorA.getValue() - valB = await editorB.getValue() - expect(valA).toEqual(valB) - expect(valB).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "randomKey0", - "_type": "block", - "children": Array [ - Object { - "_key": "randomKey1", - "_type": "span", - "marks": Array [], - "text": "Hello123 worldABC there!", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - }) - - it('will undo respective changes in different blocks correctly', async () => { - const val: PortableTextBlock[] = [ - { - _key: 'randomKey0', - _type: 'block', - markDefs: [], - style: 'normal', - children: [ - { - _key: 'randomKey1', - _type: 'span', - text: '1', - marks: [], - }, - ], - }, - { - _key: 'randomKey2', - _type: 'block', - markDefs: [], - style: 'normal', - children: [ - { - _key: 'randomKey3', - _type: 'span', - text: '2', - marks: [], - }, - ], - }, - ] - await setDocumentValue(val) - const [editorA, editorB] = await getEditors() - const startSelectionA = { - anchor: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 1}, - focus: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 1}, - backward: false, - } - await editorA.setSelection(startSelectionA) - const startSelectionB = { - anchor: {path: [{_key: 'randomKey2'}, 'children', {_key: 'randomKey3'}], offset: 1}, - focus: {path: [{_key: 'randomKey2'}, 'children', {_key: 'randomKey3'}], offset: 1}, - backward: false, - } - await editorB.setSelection(startSelectionB) - await editorA.pressKey('a') - await editorB.pressKey('b') - let valA = await editorA.getValue() - let valB = await editorB.getValue() - expect(valA).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "randomKey0", - "_type": "block", - "children": Array [ - Object { - "_key": "randomKey1", - "_type": "span", - "marks": Array [], - "text": "1a", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - Object { - "_key": "randomKey2", - "_type": "block", - "children": Array [ - Object { - "_key": "randomKey3", - "_type": "span", - "marks": Array [], - "text": "2b", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - expect(valA).toEqual(valB) - await editorA.undo() - valA = await editorA.getValue() - valB = await editorB.getValue() - expect(valA).toEqual(valB) - expect(valB).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "randomKey0", - "_type": "block", - "children": Array [ - Object { - "_key": "randomKey1", - "_type": "span", - "marks": Array [], - "text": "1", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - Object { - "_key": "randomKey2", - "_type": "block", - "children": Array [ - Object { - "_key": "randomKey3", - "_type": "span", - "marks": Array [], - "text": "2b", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - await editorB.undo() - valA = await editorA.getValue() - valB = await editorB.getValue() - expect(valA).toEqual(valB) - expect(valB).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "randomKey0", - "_type": "block", - "children": Array [ - Object { - "_key": "randomKey1", - "_type": "span", - "marks": Array [], - "text": "1", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - Object { - "_key": "randomKey2", - "_type": "block", - "children": Array [ - Object { - "_key": "randomKey3", - "_type": "span", - "marks": Array [], - "text": "2", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - const selectionA = await editorA.getSelection() - expect(selectionA).toEqual(startSelectionA) - const selectionB = await editorB.getSelection() - expect(selectionB).toEqual(startSelectionB) - await editorA.redo() - valA = await editorA.getValue() - valB = await editorB.getValue() - expect(valA).toEqual(valB) - expect(valB).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "randomKey0", - "_type": "block", - "children": Array [ - Object { - "_key": "randomKey1", - "_type": "span", - "marks": Array [], - "text": "1a", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - Object { - "_key": "randomKey2", - "_type": "block", - "children": Array [ - Object { - "_key": "randomKey3", - "_type": "span", - "marks": Array [], - "text": "2", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - await editorB.redo() - valA = await editorA.getValue() - valB = await editorB.getValue() - expect(valA).toEqual(valB) - expect(valB).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "randomKey0", - "_type": "block", - "children": Array [ - Object { - "_key": "randomKey1", - "_type": "span", - "marks": Array [], - "text": "1a", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - Object { - "_key": "randomKey2", - "_type": "block", - "children": Array [ - Object { - "_key": "randomKey3", - "_type": "span", - "marks": Array [], - "text": "2b", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - }) - - describe('unicode-rich text', () => { - it('undoing in reverse order as applied', async () => { - const initialKanji = `速ヒマヤレ誌相ルなあね日諸せ変評ホ真攻同潔ク作先た員勝どそ際接レゅ自17浅ッ実情スヤ籍認ス重力務鳥の。8平はートご多乗12青國暮整ル通国うれけこ能新ロコラハ元横ミ休探ミソ梓批ざょにね薬展むい本隣ば禁抗ワアミ部真えくト提知週むすほ。査ル人形ルおじつ政謙減セヲモ読見れレぞえ録精てざ定第ぐゆとス務接産ヤ写馬エモス聞氏サヘマ有午ごね客岡ヘロ修彩枝雨父のけリド。\n\n住ゅなぜ日16語約セヤチ任政崎ソオユ枠体ぞン古91一専泉給12関モリレネ解透ぴゃラぼ転地す球北ドざう記番重投ぼづ。期ゃ更緒リだすし夫内オ代他られくド潤刊本クヘフ伊一ウムニヘ感週け出入ば勇起ょ関図ぜ覧説めわぶ室訪おがト強車傾町コ本喰杜椿榎ほれた。暮る生的更芸窓どさはむ近問ラ入必ラニス療心コウ怒応りめけひ載総ア北吾ヌイヘ主最ニ余記エツヤ州5念稼め化浮ヌリ済毎養ぜぼ。` - await setDocumentValue(getInitialValue(initialKanji)) - const [editorA, editorB] = await getEditors() - - // Sanity-test for initial value - expect(toPlainText((await editorA.getValue()) || [])).toBe(initialKanji) - - // Editor A sets selection at start of span, and prepends "Paragraph 1: " - const desiredSelectionA = { - anchor: {path: [{_key: 'blockA'}, 'children', {_key: 'spanA'}], offset: 0}, - focus: {path: [{_key: 'blockA'}, 'children', {_key: 'spanA'}], offset: 0}, - backward: false, - } - const p1Prefix = 'Paragraph 1: ' - await editorA.setSelection(desiredSelectionA) - await editorA.insertText(p1Prefix) - - // Sanity-test for edit - const prefixedValue = `${p1Prefix}${initialKanji}` - expect(toPlainText((await editorA.getValue()) || [])).toBe(prefixedValue) - - // Editor B moves to the end of the first paragraph, and adds ` (end of paragraph 1)` - const p1Suffix = ' (end of paragraph 1)' - const p1SuffixOffset = prefixedValue.indexOf('\n\n') - await editorB.setSelection({ - anchor: {path: [{_key: 'blockA'}, 'children', {_key: 'spanA'}], offset: p1SuffixOffset}, - focus: {path: [{_key: 'blockA'}, 'children', {_key: 'spanA'}], offset: p1SuffixOffset}, - }) - await editorB.insertText(p1Suffix) - - // Editors should be in sync - let [valA, valB] = await Promise.all([editorA.getValue(), editorB.getValue()]) - expect(valA).toEqual(valB) - - // Should have the prefix from editor A, and the new suffix from editor B - const expectedPreAndPostfixedValue = prefixedValue.replace(/\n\n/, `${p1Suffix}\n\n`) - expect(toPlainText(valA || [])).toBe(expectedPreAndPostfixedValue) - - // Editor A moves to the end of the editor and adds a final `. EOL.` - eg "end of line" - const p2EOL = `. EOL.` - await editorA.setSelection({ - anchor: { - path: [{_key: 'blockA'}, 'children', {_key: 'spanA'}], - offset: expectedPreAndPostfixedValue.length, - }, - focus: { - path: [{_key: 'blockA'}, 'children', {_key: 'spanA'}], - offset: expectedPreAndPostfixedValue.length, - }, - }) - await editorA.insertText(p2EOL) - - // Editors should still be in sync - valA = await editorA.getValue() - valB = await editorB.getValue() - expect(valA).toEqual(valB) - - // And they should have the full, expected value - expect(toPlainText(valB || [])).toBe(`${expectedPreAndPostfixedValue}${p2EOL}`) - - // Editor A undos their last edit (removes `. EOL.` suffix) - await editorA.undo() - - // Ensure both editors have the same, reverted value - valA = await editorA.getValue() - valB = await editorB.getValue() - expect(valA).toEqual(valB) - expect(toPlainText(valB || [])).toBe(expectedPreAndPostfixedValue) - - // Editor B reverts their suffix - await editorB.undo() - - // Ensure we have reverted back to only the first paragraph prefix - valA = await editorA.getValue() - valB = await editorB.getValue() - expect(valA).toEqual(valB) - expect(toPlainText(valA || [])).toBe(prefixedValue) - - // Editor A undos their first prefix (we should be back to the initial value) - await editorA.undo() - valA = await editorA.getValue() - valB = await editorB.getValue() - expect(valA).toEqual(valB) - expect(toPlainText(valA || [])).toBe(initialKanji) - }) - - it('undoing out-of-order', async () => { - const initialKanji = `速ヒマヤレ誌相ルなあね日諸せ変評ホ真攻同潔ク作先た員勝どそ際接レゅ自17浅ッ実情スヤ籍認ス重力務鳥の。8平はートご多乗12青國暮整ル通国うれけこ能新ロコラハ元横ミ休探ミソ梓批ざょにね薬展むい本隣ば禁抗ワアミ部真えくト提知週むすほ。査ル人形ルおじつ政謙減セヲモ読見れレぞえ録精てざ定第ぐゆとス務接産ヤ写馬エモス聞氏サヘマ有午ごね客岡ヘロ修彩枝雨父のけリド。\n\n住ゅなぜ日16語約セヤチ任政崎ソオユ枠体ぞン古91一専泉給12関モリレネ解透ぴゃラぼ転地す球北ドざう記番重投ぼづ。期ゃ更緒リだすし夫内オ代他られくド潤刊本クヘフ伊一ウムニヘ感週け出入ば勇起ょ関図ぜ覧説めわぶ室訪おがト強車傾町コ本喰杜椿榎ほれた。暮る生的更芸窓どさはむ近問ラ入必ラニス療心コウ怒応りめけひ載総ア北吾ヌイヘ主最ニ余記エツヤ州5念稼め化浮ヌリ済毎養ぜぼ。` - await setDocumentValue(getInitialValue(initialKanji)) - const [editorA, editorB] = await getEditors() - - // Sanity-test for initial value - expect(toPlainText((await editorA.getValue()) || [])).toBe(initialKanji) - - // Editor A sets selection at start of span, and prepends "Paragraph 1: " - const desiredSelectionA = { - anchor: {path: [{_key: 'blockA'}, 'children', {_key: 'spanA'}], offset: 0}, - focus: {path: [{_key: 'blockA'}, 'children', {_key: 'spanA'}], offset: 0}, - backward: false, - } - const p1Prefix = 'Paragraph 1: ' - await editorA.setSelection(desiredSelectionA) - await editorA.insertText(p1Prefix) - - // Sanity-test for edit - const prefixedValue = `${p1Prefix}${initialKanji}` - expect(toPlainText((await editorA.getValue()) || [])).toBe(prefixedValue) - - // Editor B moves to the end of the first paragraph, and adds ` (end of paragraph 1)` - const p1Suffix = ' (end of paragraph 1)' - const p1SuffixOffset = prefixedValue.indexOf('\n\n') - await editorB.setSelection({ - anchor: {path: [{_key: 'blockA'}, 'children', {_key: 'spanA'}], offset: p1SuffixOffset}, - focus: {path: [{_key: 'blockA'}, 'children', {_key: 'spanA'}], offset: p1SuffixOffset}, - }) - await editorB.insertText(p1Suffix) - - // Editors should be in sync - let [valA, valB] = await Promise.all([editorA.getValue(), editorB.getValue()]) - expect(valA).toEqual(valB) - - // Should have the prefix from editor A, and the new suffix from editor B - const expectedPreAndPostfixedValue = prefixedValue.replace(/\n\n/, `${p1Suffix}\n\n`) - expect(toPlainText(valA || [])).toBe(expectedPreAndPostfixedValue) - - // Editor A moves to the end of the editor and adds a final `. EOL.` - eg "end of line" - const p2EOL = `. EOL.` - await editorA.setSelection({ - anchor: { - path: [{_key: 'blockA'}, 'children', {_key: 'spanA'}], - offset: expectedPreAndPostfixedValue.length, - }, - focus: { - path: [{_key: 'blockA'}, 'children', {_key: 'spanA'}], - offset: expectedPreAndPostfixedValue.length, - }, - }) - await editorA.insertText(p2EOL) - - // Editors should still be in sync - valA = await editorA.getValue() - valB = await editorB.getValue() - expect(valA).toEqual(valB) - - // And they should have the full, expected value - expect(toPlainText(valB || [])).toBe(`${expectedPreAndPostfixedValue}${p2EOL}`) - - // Editor A undos their last edit (removes `. EOL.` suffix) - await editorA.undo() - - // Ensure both editors have the same, reverted value - valA = await editorA.getValue() - valB = await editorB.getValue() - expect(valA).toEqual(valB) - expect(toPlainText(valB || [])).toBe(expectedPreAndPostfixedValue) - - // Note how Editor B does _not_ revert their suffix here, but with two editor A undos, - // the editor prefix (`Paragraph 1: `) and suffix (`. EOL.`) should both be gone, and - // Editor B's edit (the ` (end of paragraph 1)`) suffix should be left in place. - // The opposite case is handled in test above ("reverse order") - await editorA.undo() - - const expectedEditorARollback = initialKanji.replace(/\n\n/, `${p1Suffix}\n\n`) - valA = await editorA.getValue() - valB = await editorB.getValue() - expect(valA).toEqual(valB) - expect(toPlainText(valA || [])).toBe(expectedEditorARollback) - - // Editor B undos _their_ edit (removes ` (end of paragraph 1)` suffix), - // meaning we should be back to the original value - await editorB.undo() - valA = await editorA.getValue() - valB = await editorB.getValue() - expect(valA).toEqual(valB) - expect(toPlainText(valA || [])).toBe(initialKanji) - }) - - it('editor A undo their change after B did an unrelated change (single-line, emoji)', async () => { - const [beginning, middle, end] = [ - 'A curious 🦊 named Felix lived in the 🪄🌲 of Willowwood. One day, he discovered a mysterious 🕳️, which lead to a magical 🌌. ', - 'In the 🪐 of Celestia, 🦊 met a friendly 🌈🦄 named Sparkle. ', - 'They had extraordinary adventures together, befriending a 🧚, who gave them so many 📚 that they never lacked for reading material!', - ] - const initialText = `${beginning}${end}` - await setDocumentValue(getInitialValue(initialText)) - const [editorA, editorB] = await getEditors() - - // Sanity-test for initial value - expect(toPlainText((await editorA.getValue()) || [])).toBe(initialText) - - // Editor A sets selection at end of the text (after the character `!`), and removes it - const charOffset = initialText.indexOf('!') + 1 - const desiredSelectionA = { - anchor: {path: [{_key: 'blockA'}, 'children', {_key: 'spanA'}], offset: charOffset}, - focus: {path: [{_key: 'blockA'}, 'children', {_key: 'spanA'}], offset: charOffset}, - backward: false, - } - await editorA.setSelection(desiredSelectionA) - await editorA.pressKey('Backspace') - - // Sanity-test for edit - const expectedEditedText = initialText.slice(0, -1) - expect(toPlainText((await editorA.getValue()) || [])).toBe(expectedEditedText) - expect(await editorB.getValue()).toEqual(await editorA.getValue()) - - // Editor B adds some new text in the middle of the span (after `🌌. `) - await editorB.setSelection({ - anchor: {path: [{_key: 'blockA'}, 'children', {_key: 'spanA'}], offset: 127}, - focus: {path: [{_key: 'blockA'}, 'children', {_key: 'spanA'}], offset: 127}, - }) - - await editorB.insertText(middle) - - // Editors should be in sync - let valA = await editorA.getValue() - let valB = await editorB.getValue() - expect(valA).toEqual(valB) - - // Should have the untouched start of the story, the newly inserted middle, and the edit removing the trailing `!` - const expectedEditedEnd = end.slice(0, -1) - expect(toPlainText(valA || [])).toBe(`${beginning}${middle}${expectedEditedEnd}`) - - // Editor A undos their edit (`reading material` => `reading material!`) - await editorA.undo() - - // Editors should still be in sync - valA = await editorA.getValue() - valB = await editorB.getValue() - expect(valA).toEqual(valB) - - // Shape of editor should be the expected full story - expect(toPlainText(valA || [])).toBe(`${beginning}${middle}${end}`) - - // Sanity-check that the value is still only a single block, not multiple - expect(valA || []).toHaveLength(1) - }) - - it('editor A undo their change after B did an unrelated change (multi-line block, emoji)', async () => { - const initialText = `In the 🪐 of Celestia, 🦊 met a friendly 🌈🦄 named Sparkle.\n\nThey had extraordinary adventures together, befriending a 🧚, who gave them so many 📚 that they never lacked for reading material!` - await setDocumentValue(getInitialValue(initialText)) - const [editorA, editorB] = await getEditors() - - // Sanity-test for initial value - expect(toPlainText((await editorA.getValue()) || [])).toBe(initialText) - - // Editor A sets selection at end of the second paragraph (after the character `!`), and removes it - const charOffset = initialText.indexOf('!') + 1 - const desiredSelectionA = { - anchor: {path: [{_key: 'blockA'}, 'children', {_key: 'spanA'}], offset: charOffset}, - focus: {path: [{_key: 'blockA'}, 'children', {_key: 'spanA'}], offset: charOffset}, - backward: false, - } - await editorA.setSelection(desiredSelectionA) - await editorA.pressKey('Backspace') - - // Sanity-test for edit - const expectedEditedText = initialText.replace(/!/g, '') - expect(toPlainText((await editorA.getValue()) || [])).toBe(expectedEditedText) - expect(await editorB.getValue()).toEqual(await editorA.getValue()) - - // Editor B adds a new paragraph (_within same block_) to the start of the editor - const newPrefix = - 'A curious 🦊 named Felix lived in the 🪄🌲 of Willowwood. One day, he discovered a mysterious 🕳️, which lead to a magical 🌌.\n\n' - await editorB.setSelection({ - anchor: {path: [{_key: 'blockA'}, 'children', {_key: 'spanA'}], offset: 0}, - focus: {path: [{_key: 'blockA'}, 'children', {_key: 'spanA'}], offset: 0}, - }) - await editorB.insertText(newPrefix) - - // Editors should be in sync - let valA = await editorA.getValue() - let valB = await editorB.getValue() - expect(valA).toEqual(valB) - - // Should have the edit from editor A, and the new paragraph from editor B - expect(toPlainText(valA || [])).toBe(newPrefix + expectedEditedText) - - // Editor A undos their edit (`reading material` => `reading material!`) - await editorA.undo() - - // Editors should still be in sync - valA = await editorA.getValue() - valB = await editorB.getValue() - expect(valA).toEqual(valB) - - // Sanity-check that the value is still only a single block, not multiple - expect(valA || []).toHaveLength(1) - - // Shape of editor should be the expected prefix + initial value - expect(toPlainText(valA || [])).toBe(newPrefix + initialText) - }) - - it('editor A undo their change after B did an unrelated change (multi-line block, kanji)', async () => { - const initialText = `彼は、偉大な番兵がまさに尾根の頂上にいて、裸足では地面から最も低い枝にあることを知っていた。 彼は腹ばいになって雪と泥の中に滑り込み、下の何もない空き地を見下ろした。\n\n彼の心臓は胸の中で止まった。 しばらくの間、彼は息をすることさえできなかった。 月明かりは空き地、キャンプファイヤーの灰、雪に覆われた斜面、大きな岩、半分凍った小さな小川を照らしていました。すべては数1時間前とまったく同じでした。` - await setDocumentValue(getInitialValue(initialText)) - const [editorA, editorB] = await getEditors() - - // Sanity-test for initial value - expect(toPlainText((await editorA.getValue()) || [])).toBe(initialText) - - // Editor A sets selection at end of the second paragraph (after the number `1`), and removes it - const charOffset = initialText.indexOf('1') + 1 - const desiredSelectionA = { - anchor: {path: [{_key: 'blockA'}, 'children', {_key: 'spanA'}], offset: charOffset}, - focus: {path: [{_key: 'blockA'}, 'children', {_key: 'spanA'}], offset: charOffset}, - backward: false, - } - await editorA.setSelection(desiredSelectionA) - await editorA.pressKey('Backspace') - - // Sanity-test for edit - const expectedEditedText = initialText.replace(/数1/g, '数') - expect(toPlainText((await editorA.getValue()) || [])).toBe(expectedEditedText) - expect(await editorB.getValue()).toEqual(await editorA.getValue()) - - // Editor B adds a new paragraph (_within same block_) to the start of the editor - const newPrefix = - '彼の背後で、領主のリングメイルの柔らかい金属の滑り音、葉のカサカサ音、そして伸びた枝が彼の長剣を掴み、華麗なクロテンのマントを引っ張りながら呪いの言葉をつぶやくのが聞こえた。\n\n' - await editorB.setSelection({ - anchor: {path: [{_key: 'blockA'}, 'children', {_key: 'spanA'}], offset: 0}, - focus: {path: [{_key: 'blockA'}, 'children', {_key: 'spanA'}], offset: 0}, - }) - await editorB.insertText(newPrefix) - - // Editors should be in sync - let valA = await editorA.getValue() - let valB = await editorB.getValue() - expect(valA).toEqual(valB) - - // Should have the edit from editor A, and the new paragraph from editor B - expect(toPlainText(valA || [])).toBe(newPrefix + expectedEditedText) - - // Editor A undos their edit (`数` => `数1`) - await editorA.undo() - - // Editors should still be in sync - valA = await editorA.getValue() - valB = await editorB.getValue() - expect(valA).toEqual(valB) - - // Shape of editor should be the expected prefix + initial value - expect(toPlainText(valA || [])).toBe(newPrefix + initialText) - - // Sanity-check that the value is still only a single block, not multiple - expect(valA || []).toHaveLength(1) - }) - }) -}) diff --git a/packages/@sanity/portable-text-editor/e2e-tests/__tests__/writingTogether.collaborative.test.ts b/packages/@sanity/portable-text-editor/e2e-tests/__tests__/writingTogether.collaborative.test.ts deleted file mode 100644 index 02bf3efecee..00000000000 --- a/packages/@sanity/portable-text-editor/e2e-tests/__tests__/writingTogether.collaborative.test.ts +++ /dev/null @@ -1,859 +0,0 @@ -/** @jest-environment ./setup/collaborative.jest.env.ts */ -import '../setup/globals.jest' - -import {describe, expect, it} from '@jest/globals' -import {type PortableTextBlock} from '@sanity/types' - -const initialValue: PortableTextBlock[] = [ - { - _key: 'randomKey0', - _type: 'block', - markDefs: [], - style: 'normal', - children: [{_key: 'randomKey1', _type: 'span', text: 'Hello', marks: []}], - }, -] - -describe('collaborate editing', () => { - it('will have the same start value for editor A and B', async () => { - await setDocumentValue(initialValue) - const editors = await getEditors() - const valA = await editors[0].getValue() - const valB = await editors[1].getValue() - expect(valA).toEqual(initialValue) - expect(valB).toEqual(initialValue) - }) - - it('will update value in editor B when editor A writes something', async () => { - await setDocumentValue(initialValue) - const [editorA, editorB] = await getEditors() - await editorA.setSelection({ - anchor: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 11}, - focus: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 11}, - }) - await editorA.insertText(' world') - const valA = await editorA.getValue() - const valB = await editorB.getValue() - expect(valA).toEqual(valB) - expect(valB).toEqual([ - { - _key: 'randomKey0', - _type: 'block', - markDefs: [], - style: 'normal', - children: [ - { - _key: 'randomKey1', - _type: 'span', - text: 'Hello world', - marks: [], - }, - ], - }, - ]) - const selectionA = await editorA.getSelection() - const selectionB = await editorB.getSelection() - expect(selectionA).toEqual({ - anchor: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 11}, - focus: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 11}, - backward: false, - }) - expect(selectionB).toEqual({ - anchor: {offset: 0, path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}]}, - focus: {offset: 0, path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}]}, - backward: false, - }) - }) - - it('should not remove content for both users if one user backspaces into a block that starts with a mark.', async () => { - const exampleValue = [ - { - _key: 'randomKey0', - _type: 'block', - children: [ - { - _key: 'randomKey1', - _type: 'span', - marks: ['strong'], - text: 'Example Text: ', - }, - { - _type: 'span', - marks: [], - text: "This is a very long example text that will completely disappear later on. It's kind of a bad magic trick, really. Just writing more text so the disappearance becomes more apparent. This is a very long example text that will completely disappear later on. It's kind of a bad magic trick, really. Just writing more text so the disappearance becomes more apparent. This is a very long example text that will completely disappear later on. It's kind of a bad magic trick, really. Just writing more text so the disappearance becomes more apparent.", - _key: 'randomKey2', - }, - ], - markDefs: [], - style: 'normal', - }, - ] - await setDocumentValue(exampleValue) - const [editorA, editorB] = await getEditors() - await editorA.setSelection({ - anchor: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey2'}], offset: 542}, - focus: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey2'}], offset: 542}, - }) - await editorA.pressKey('Enter') - await editorA.pressKey('Backspace') - - await new Promise((resolve) => setTimeout(resolve, 1000)) - - const valA = await editorA.getValue() - const valB = await editorB.getValue() - expect(valA).toEqual(valB) - expect(valB).toEqual([ - { - _key: 'randomKey0', - _type: 'block', - children: [ - { - _key: 'randomKey1', - _type: 'span', - marks: ['strong'], - text: 'Example Text: ', - }, - { - _type: 'span', - marks: [], - text: "This is a very long example text that will completely disappear later on. It's kind of a bad magic trick, really. Just writing more text so the disappearance becomes more apparent. This is a very long example text that will completely disappear later on. It's kind of a bad magic trick, really. Just writing more text so the disappearance becomes more apparent. This is a very long example text that will completely disappear later on. It's kind of a bad magic trick, really. Just writing more text so the disappearance becomes more apparent.", - _key: 'randomKey2', - }, - ], - markDefs: [], - style: 'normal', - }, - ]) - }) - - it('will reset the value when someone deletes everything, and when they start to type again, they will produce their own respective blocks.', async () => { - await setDocumentValue(initialValue) - const [editorA, editorB] = await getEditors() - await editorA.setSelection({ - anchor: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 11}, - focus: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 11}, - }) - await editorA.insertText(' world') - let valA = await editorA.getValue() - let valB = await editorB.getValue() - expect(valA).toEqual(valB) - expect(valB).toEqual([ - { - _key: 'randomKey0', - _type: 'block', - markDefs: [], - style: 'normal', - children: [ - { - _key: 'randomKey1', - _type: 'span', - text: 'Hello world', - marks: [], - }, - ], - }, - ]) - await editorA.pressKey('Backspace', 11) - valA = await editorA.getValue() - valB = await editorB.getValue() - expect(valA).toEqual(valB) - expect(valA).toBe(undefined) - await editorB.pressKey('1') - valA = await editorA.getValue() - valB = await editorB.getValue() - expect(valA).toEqual(valB) - expect(valA).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "B-9", - "_type": "block", - "children": Array [ - Object { - "_key": "B-8", - "_type": "span", - "marks": Array [], - "text": "1", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - await editorA.pressKey('2') - valA = await editorA.getValue() - valB = await editorB.getValue() - expect(valA).toEqual(valB) - expect(valB).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "B-9", - "_type": "block", - "children": Array [ - Object { - "_key": "B-8", - "_type": "span", - "marks": Array [], - "text": "12", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - }) - - it('will update value in editor A when editor B writes something', async () => { - await setDocumentValue([ - { - _key: 'randomKey0', - _type: 'block', - markDefs: [], - style: 'normal', - children: [ - { - _key: 'randomKey1', - _type: 'span', - text: 'Hello world', - marks: [], - }, - ], - }, - ]) - const [editorA, editorB] = await getEditors() - const desiredSelectionA = { - anchor: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 18}, - focus: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 18}, - backward: false, - } - await editorA.setSelection(desiredSelectionA) - await editorB.setSelection({ - anchor: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 11}, - focus: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 11}, - }) - await editorB.insertText(' there!') - const valA = await editorA.getValue() - const valB = await editorB.getValue() - expect(valA).toEqual(valB) - expect(valB).toEqual([ - { - _key: 'randomKey0', - _type: 'block', - markDefs: [], - style: 'normal', - children: [ - { - _key: 'randomKey1', - _type: 'span', - text: 'Hello world there!', - marks: [], - }, - ], - }, - ]) - const selectionA = await editorA.getSelection() - expect(selectionA).toEqual(desiredSelectionA) - const selectionB = await editorB.getSelection() - expect(selectionB).toEqual({ - anchor: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 18}, - focus: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 18}, - backward: false, - }) - }) - - it('will let editor A stay at the current position on line 1 while editor B inserts a new line below', async () => { - await setDocumentValue([ - { - _key: 'randomKey0', - _type: 'block', - markDefs: [], - style: 'normal', - children: [ - { - _key: 'randomKey1', - _type: 'span', - text: 'Hello world there!', - marks: [], - }, - ], - }, - ]) - const [editorA, editorB] = await getEditors() - const desiredSelectionA = { - anchor: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 11}, - focus: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 11}, - backward: false, - } - await editorB.setSelection({ - anchor: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 18}, - focus: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 18}, - }) - await editorA.setSelection(desiredSelectionA) - await editorB.pressKey('Enter') - const valA = await editorA.getValue() - const valB = await editorB.getValue() - expect(valA).toEqual(valB) - expect(valB).toEqual([ - { - _key: 'randomKey0', - _type: 'block', - markDefs: [], - style: 'normal', - children: [ - { - _key: 'randomKey1', - _type: 'span', - text: 'Hello world there!', - marks: [], - }, - ], - }, - { - _key: 'B-6', - _type: 'block', - children: [ - { - _key: 'B-5', - _type: 'span', - marks: [], - text: '', - }, - ], - markDefs: [], - style: 'normal', - }, - ]) - const selectionA = await editorA.getSelection() - const selectionB = await editorB.getSelection() - expect(selectionA).toEqual(desiredSelectionA) - expect(selectionB).toEqual({ - anchor: {offset: 0, path: [{_key: 'B-6'}, 'children', {_key: 'B-5'}]}, - focus: {offset: 0, path: [{_key: 'B-6'}, 'children', {_key: 'B-5'}]}, - backward: false, - }) - }) - - it('will update value in editor A when editor B writes something while A stays on current line and position', async () => { - await setDocumentValue([ - { - _key: 'randomKey0', - _type: 'block', - markDefs: [], - style: 'normal', - children: [ - { - _key: 'randomKey1', - _type: 'span', - text: 'Hello world there!', - marks: [], - }, - ], - }, - { - _key: 'B-3', - _type: 'block', - children: [ - { - _key: 'B-2', - _type: 'span', - marks: [], - text: '', - }, - ], - markDefs: [], - style: 'normal', - }, - ]) - const [editorA, editorB] = await getEditors() - await editorA.setSelection({ - anchor: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 11}, - focus: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 11}, - }) - await editorB.setSelection({ - anchor: {offset: 0, path: [{_key: 'B-3'}, 'children', {_key: 'B-2'}]}, - focus: {offset: 0, path: [{_key: 'B-3'}, 'children', {_key: 'B-2'}]}, - }) - await editorB.insertText("I'm writing here") - await editorB.pressKey('!') - const valA = await editorA.getValue() - const valB = await editorB.getValue() - - expect(Array.isArray(valB)).toBe(true) - if (!Array.isArray(valB)) { - throw new Error('Editor value did not return an array') // For typescript, should throw from assertion above - } - - expect(valA).toEqual(valB) - expect(valB && valB[1]).toEqual({ - _key: 'B-3', - _type: 'block', - children: [ - { - _key: 'B-2', - _type: 'span', - marks: [], - text: "I'm writing here!", - }, - ], - markDefs: [], - style: 'normal', - }) - const selectionA = await editorA.getSelection() - const selectionB = await editorB.getSelection() - expect(selectionA).toEqual({ - anchor: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 11}, - focus: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 11}, - backward: false, - }) - expect(selectionB).toEqual({ - anchor: {offset: 17, path: [{_key: 'B-3'}, 'children', {_key: 'B-2'}]}, - focus: {offset: 17, path: [{_key: 'B-3'}, 'children', {_key: 'B-2'}]}, - backward: false, - }) - }) - - it('will update value in editor B when editor A writes something while B stays on current line and position', async () => { - await setDocumentValue([ - { - _key: 'randomKey0', - _type: 'block', - markDefs: [], - style: 'normal', - children: [ - { - _key: 'randomKey1', - _type: 'span', - text: 'Hello world there!', - marks: [], - }, - ], - }, - { - _key: 'B-3', - _type: 'block', - children: [ - { - _key: 'B-2', - _type: 'span', - marks: [], - text: "I'm writing here!", - }, - ], - markDefs: [], - style: 'normal', - }, - ]) - const [editorA, editorB] = await getEditors() - const startSelectionA = { - anchor: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 11}, - focus: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 11}, - backward: false, - } - await editorA.setSelection(startSelectionA) - await editorB.setSelection({ - anchor: {offset: 17, path: [{_key: 'B-3'}, 'children', {_key: 'B-2'}]}, - focus: {offset: 17, path: [{_key: 'B-3'}, 'children', {_key: 'B-2'}]}, - }) - const selectionABefore = await editorA.getSelection() - expect(selectionABefore).toEqual(startSelectionA) - await editorA.insertText('<- I left off here. And you wrote that ->') - const valA = await editorA.getValue() - const valB = await editorB.getValue() - expect(valA).toEqual(valB) - - if (!Array.isArray(valA)) { - throw new Error('Editor value did not return an array') // For typescript, shouldn't happen - } - - expect(valA[0]).toEqual({ - _key: 'randomKey0', - _type: 'block', - markDefs: [], - style: 'normal', - children: [ - { - _key: 'randomKey1', - _type: 'span', - text: 'Hello world<- I left off here. And you wrote that -> there!', - marks: [], - }, - ], - }) - const selectionA = await editorA.getSelection() - const selectionB = await editorB.getSelection() - expect(selectionA).toEqual({ - anchor: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 52}, - focus: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 52}, - backward: false, - }) - expect(selectionB).toEqual({ - anchor: {offset: 17, path: [{_key: 'B-3'}, 'children', {_key: 'B-2'}]}, - focus: {offset: 17, path: [{_key: 'B-3'}, 'children', {_key: 'B-2'}]}, - backward: false, - }) - }) - - it('will let B stay on same line when A inserts a new line above', async () => { - await setDocumentValue([ - { - _key: 'randomKey0', - _type: 'block', - markDefs: [], - style: 'normal', - children: [ - { - _key: 'randomKey1', - _type: 'span', - text: 'Hello world<- I left off here. And you wrote that -> there!', - marks: [], - }, - ], - }, - { - _key: 'B-3', - _type: 'block', - children: [ - { - _key: 'B-2', - _type: 'span', - marks: [], - text: "I'm writing here!", - }, - ], - markDefs: [], - style: 'normal', - }, - ]) - const [editorA, editorB] = await getEditors() - const valAa = await editorA.getValue() - const valBb = await editorB.getValue() - expect(valAa).toEqual(valBb) - const newExpectedSelA = { - anchor: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 52}, - focus: {path: [{_key: 'randomKey0'}, 'children', {_key: 'randomKey1'}], offset: 52}, - backward: false, - } - await editorA.setSelection(newExpectedSelA) - const newSelA = await editorA.getSelection() - expect(newExpectedSelA).toEqual(newSelA) - await editorB.setSelection({ - anchor: {offset: 17, path: [{_key: 'B-3'}, 'children', {_key: 'B-2'}]}, - focus: {offset: 17, path: [{_key: 'B-3'}, 'children', {_key: 'B-2'}]}, - }) - await editorA.pressKey('Enter') - const valA = await editorA.getValue() - const valB = await editorB.getValue() - expect(valA).toEqual(valB) - expect(valA).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "randomKey0", - "_type": "block", - "children": Array [ - Object { - "_key": "randomKey1", - "_type": "span", - "marks": Array [], - "text": "Hello world<- I left off here. And you wrote that ->", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - Object { - "_key": "A-6", - "_type": "block", - "children": Array [ - Object { - "_key": "A-5", - "_type": "span", - "marks": Array [], - "text": " there!", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - Object { - "_key": "B-3", - "_type": "block", - "children": Array [ - Object { - "_key": "B-2", - "_type": "span", - "marks": Array [], - "text": "I'm writing here!", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - expect(await editorA.getSelection()).toMatchInlineSnapshot(` - Object { - "anchor": Object { - "offset": 0, - "path": Array [ - Object { - "_key": "A-6", - }, - "children", - Object { - "_key": "A-5", - }, - ], - }, - "backward": false, - "focus": Object { - "offset": 0, - "path": Array [ - Object { - "_key": "A-6", - }, - "children", - Object { - "_key": "A-5", - }, - ], - }, - } - `) - await editorA.pressKey('Enter') - const valAAfterSecondEnter = await editorA.getValue() - expect(valAAfterSecondEnter).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "randomKey0", - "_type": "block", - "children": Array [ - Object { - "_key": "randomKey1", - "_type": "span", - "marks": Array [], - "text": "Hello world<- I left off here. And you wrote that ->", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - Object { - "_key": "A-9", - "_type": "block", - "children": Array [ - Object { - "_key": "A-8", - "_type": "span", - "marks": Array [], - "text": "", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - Object { - "_key": "A-6", - "_type": "block", - "children": Array [ - Object { - "_key": "A-5", - "_type": "span", - "marks": Array [], - "text": " there!", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - Object { - "_key": "B-3", - "_type": "block", - "children": Array [ - Object { - "_key": "B-2", - "_type": "span", - "marks": Array [], - "text": "I'm writing here!", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - const selectionA = await editorA.getSelection() - expect(selectionA).toEqual({ - anchor: {path: [{_key: 'A-6'}, 'children', {_key: 'A-5'}], offset: 0}, - focus: {path: [{_key: 'A-6'}, 'children', {_key: 'A-5'}], offset: 0}, - backward: false, - }) - const selectionB = await editorB.getSelection() - expect(selectionB).toEqual({ - anchor: {offset: 17, path: [{_key: 'B-3'}, 'children', {_key: 'B-2'}]}, - focus: {offset: 17, path: [{_key: 'B-3'}, 'children', {_key: 'B-2'}]}, - backward: false, - }) - }) - - it('diffMatchPatch works as expected', async () => { - await setDocumentValue([ - { - _key: '26901064a3c9', - _type: 'block', - children: [ - { - _key: 'b629e8140c25', - _type: 'span', - marks: [], - text: 'pweoirrporiwpweporiwproi wer', - }, - { - _key: 'ef4627c1c11b', - _type: 'span', - marks: ['strong'], - text: 'poiwyuXty45........ytutyy666uerpwer1', - }, - { - _key: '7d3c9bcc9c10', - _type: 'span', - marks: [], - text: 'weuirwer werewopri', - }, - ], - markDefs: [], - style: 'normal', - }, - ]) - const selectionA = { - anchor: {path: [{_key: '26901064a3c9'}, 'children', {_key: 'ef4627c1c11b'}], offset: 15}, - focus: {path: [{_key: '26901064a3c9'}, 'children', {_key: 'ef4627c1c11b'}], offset: 15}, - } - const [editorA, editorB] = await getEditors() - await editorA.setSelection(selectionA) - await editorA.insertText('!') - const valueB = await editorB.getValue() - expect(valueB).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "26901064a3c9", - "_type": "block", - "children": Array [ - Object { - "_key": "b629e8140c25", - "_type": "span", - "marks": Array [], - "text": "pweoirrporiwpweporiwproi wer", - }, - Object { - "_key": "ef4627c1c11b", - "_type": "span", - "marks": Array [ - "strong", - ], - "text": "poiwyuXty45....!....ytutyy666uerpwer1", - }, - Object { - "_key": "7d3c9bcc9c10", - "_type": "span", - "marks": Array [], - "text": "weuirwer werewopri", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - const valueA = await editorA.getValue() - expect(valueA).toEqual(valueB) - const newSelectionA = await editorA.getSelection() - expect(newSelectionA).toEqual({ - anchor: {path: [{_key: '26901064a3c9'}, 'children', {_key: 'ef4627c1c11b'}], offset: 16}, - focus: {path: [{_key: '26901064a3c9'}, 'children', {_key: 'ef4627c1c11b'}], offset: 16}, - backward: false, - }) - }) - it('will not result in duplicate keys when overwriting some partial bold text line, as the only content in the editor', async () => { - const [editorA, editorB] = await getEditors() - await editorA.insertText('Hey') - await editorA.toggleMark('b') - await editorA.insertText('there') - const valA = await editorA.getValue() - if (!valA || !Array.isArray(valA[0].children)) { - throw new Error('Unexpected value') - } - await editorA.setSelection({ - anchor: { - path: [{_key: valA[0]._key}, 'children', {_key: valA[0].children[0]._key}], - offset: 0, - }, - focus: { - path: [{_key: valA[0]._key}, 'children', {_key: valA[0].children[1]._key}], - offset: 5, - }, - }) - await editorA.insertText('1') - const newValA = await editorA.getValue() - expect(newValA).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "A-4", - "_type": "block", - "children": Array [ - Object { - "_key": "A-3", - "_type": "span", - "marks": Array [], - "text": "1", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - const valB = await editorB.getValue() - expect(newValA).toEqual(valB) - }) - it('sends the correct patches when toggling marks in a sequence', async () => { - const [editorA, editorB] = await getEditors() - await editorA.toggleMark('b') - await editorA.insertText('Bold') - await editorA.toggleMark('b') - await editorA.toggleMark('i') - await editorA.insertText('Italic') - await editorA.toggleMark('i') - const valueA = await editorA.getValue() - const valueB = await editorB.getValue() - expect(valueB).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "A-4", - "_type": "block", - "children": Array [ - Object { - "_key": "A-5", - "_type": "span", - "marks": Array [ - "strong", - ], - "text": "Bold", - }, - Object { - "_key": "A-6", - "_type": "span", - "marks": Array [ - "em", - ], - "text": "Italic", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - expect(valueA).toEqual(valueB) - }) -}) diff --git a/packages/@sanity/portable-text-editor/e2e-tests/e2e.config.cjs b/packages/@sanity/portable-text-editor/e2e-tests/e2e.config.cjs deleted file mode 100644 index aea2aa4af79..00000000000 --- a/packages/@sanity/portable-text-editor/e2e-tests/e2e.config.cjs +++ /dev/null @@ -1,13 +0,0 @@ -'use strict' - -const {createJestConfig} = require('../../../../test/config.cjs') - -// eslint-disable-next-line no-console -console.info('Running collaborate editing tests for the Portable Text Editor') - -module.exports = createJestConfig({ - displayName: require('../package.json').name, - globalSetup: './setup/globalSetup.ts', - globalTeardown: './setup/globalTeardown.ts', - setupFilesAfterEnv: ['./setup/afterEnv.ts'], -}) diff --git a/packages/@sanity/portable-text-editor/e2e-tests/schema.ts b/packages/@sanity/portable-text-editor/e2e-tests/schema.ts deleted file mode 100644 index 8c8814c1217..00000000000 --- a/packages/@sanity/portable-text-editor/e2e-tests/schema.ts +++ /dev/null @@ -1,34 +0,0 @@ -import {defineType} from '@sanity/types' - -export const imageType = { - type: 'image', - name: 'blockImage', -} - -export const someObject = { - type: 'object', - name: 'someObject', - fields: [{type: 'string', name: 'color'}], -} - -export const blockType = { - type: 'block', - name: 'block', - styles: [ - {title: 'Normal', value: 'normal'}, - {title: 'H1', value: 'h1'}, - {title: 'H2', value: 'h2'}, - {title: 'H3', value: 'h3'}, - {title: 'H4', value: 'h4'}, - {title: 'H5', value: 'h5'}, - {title: 'H6', value: 'h6'}, - {title: 'Quote', value: 'blockquote'}, - ], - of: [someObject], -} - -export const portableTextType = defineType({ - type: 'array', - name: 'body', - of: [blockType, someObject], -}) diff --git a/packages/@sanity/portable-text-editor/e2e-tests/serve.ts b/packages/@sanity/portable-text-editor/e2e-tests/serve.ts deleted file mode 100644 index 8be81ba0867..00000000000 --- a/packages/@sanity/portable-text-editor/e2e-tests/serve.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Start servers file for 'npm start' - -import globalSetup from './setup/globalSetup' - -globalSetup().then(() => { - // eslint-disable-next-line no-console - console.log( - 'Started web and websocket servers.\n\nhttp://localhost:3000 (web)\n\nhttp://localhost:3001 (ws)', - ) -}) diff --git a/packages/@sanity/portable-text-editor/e2e-tests/setup/afterEnv.ts b/packages/@sanity/portable-text-editor/e2e-tests/setup/afterEnv.ts deleted file mode 100644 index 439a72e51bc..00000000000 --- a/packages/@sanity/portable-text-editor/e2e-tests/setup/afterEnv.ts +++ /dev/null @@ -1,5 +0,0 @@ -import {jest} from '@jest/globals' - -jest.setTimeout(20 * 1000) - -export {} diff --git a/packages/@sanity/portable-text-editor/e2e-tests/setup/collaborative.jest.env.ts b/packages/@sanity/portable-text-editor/e2e-tests/setup/collaborative.jest.env.ts deleted file mode 100644 index 4ebccc2c981..00000000000 --- a/packages/@sanity/portable-text-editor/e2e-tests/setup/collaborative.jest.env.ts +++ /dev/null @@ -1,347 +0,0 @@ -import { - type Browser, - type BrowserContext, - chromium, - type ElementHandle, - type Page, -} from '@playwright/test' -import {type PortableTextBlock} from '@sanity/types' -import NodeEnvironment from 'jest-environment-node' -import {isEqual} from 'lodash' -import ipc from 'node-ipc' - -import {type EditorSelection} from '../../src' -import {normalizeSelection} from '../../src/utils/selection' - -ipc.config.id = 'collaborative-jest-environment-ipc-client' -ipc.config.retry = 5000 -ipc.config.networkPort = 3002 -ipc.config.silent = true - -const WEB_SERVER_ROOT_URL = 'http://localhost:3000' - -// Forward debug info from the PTE in the browsers -// const DEBUG = 'sanity-pte:*' -// eslint-disable-next-line no-process-env -const DEBUG = process.env.DEBUG || false - -// Wait this long for selections to appear in the browser -// This should be set high to support slower host systems. -const SELECTION_TIMEOUT_MS = 5000 - -// How long to wait for a new revision to come back to the client(s) when patched through the server. -// This should be set high to support slower host systems. -const REVISION_TIMEOUT_MS = 5000 - -export default class CollaborationEnvironment extends NodeEnvironment { - private _browserA?: Browser - private _browserB?: Browser - private _pageA?: Page - private _pageB?: Page - private _contextA?: BrowserContext - private _contextB?: BrowserContext - - // Saving these setup/teardown functions here for future reference. - // public async setup(): Promise { - // await super.setup() - // } - // public async teardown(): Promise { - // await super.teardown() - // } - - public async handleTestEvent(event: {name: string}): Promise { - if (event.name === 'run_start') { - await this._setupInstance() - } - if (event.name == 'test_start') { - await this._createNewTestPage() - } - if (event.name === 'run_finish') { - await this._destroyInstance() - } - } - - private async _setupInstance(): Promise { - ipc.connectToNet('socketServer') - this._browserA = await chromium.launch() - this._browserB = await chromium.launch() - const contextA = await this._browserA.newContext() - const contextB = await this._browserB.newContext() - await contextA.grantPermissions(['clipboard-read', 'clipboard-write']) - await contextB.grantPermissions(['clipboard-read', 'clipboard-write']) - this._contextA = contextA - this._contextB = contextB - this._pageA = await this._contextA.newPage() - this._pageB = await this._contextB.newPage() - } - - private async _destroyInstance(): Promise { - await this._pageA?.close() - await this._pageB?.close() - await this._browserA?.close() - await this._browserB?.close() - ipc.disconnect('socketServer') - } - - private async _createNewTestPage(): Promise { - if (!this._pageA || !this._pageB) { - throw new Error('Page not initialized') - } - - // This will identify this test throughout the web environment - const testId = (Math.random() + 1).toString(36).slice(7) - - // Hook up page console and npm debug in the PTE - if (DEBUG) { - await this._pageA.addInitScript((filter: string) => { - window.localStorage.debug = filter - }, DEBUG) - await this._pageB.addInitScript((filter: string) => { - window.localStorage.debug = filter - }, DEBUG) - this._pageA.on('console', (message) => - // eslint-disable-next-line no-console - console.log(`A:${message.type().slice(0, 3).toUpperCase()} ${message.text()}`), - ) - this._pageB.on('console', (message) => - // eslint-disable-next-line no-console - console.log(`B:${message.type().slice(0, 3).toUpperCase()} ${message.text()}`), - ) - } - this._pageA.on('pageerror', (err) => { - console.error('Editor A crashed', err) - throw err - }) - this._pageB.on('pageerror', (err) => { - console.error('Editor B crashed', err) - throw err - }) - - this.global.setDocumentValue = async ( - value: PortableTextBlock[] | undefined, - ): Promise => { - const revId = (Math.random() + 1).toString(36).slice(7) - ipc.of.socketServer.emit('payload', JSON.stringify({type: 'value', value, testId, revId})) - await this._pageA?.waitForSelector(`code[data-rev-id="${revId}"]`, { - timeout: REVISION_TIMEOUT_MS, - }) - await this._pageB?.waitForSelector(`code[data-rev-id="${revId}"]`, { - timeout: REVISION_TIMEOUT_MS, - }) - } - - this.global.getEditors = () => - Promise.all( - [this._pageA!, this._pageB!].map(async (page, index) => { - const userAgent = await page.evaluate(() => navigator.userAgent) - const isMac = /Mac|iPod|iPhone|iPad/.test(userAgent) - const metaKey = isMac ? 'Meta' : 'Control' - const editorId = `${['A', 'B'][index]}${testId}` - const [ - editableHandle, - selectionHandle, - valueHandle, - revIdHandle, - ]: (ElementHandle | null)[] = await Promise.all([ - page.waitForSelector('div[contentEditable="true"]'), - page.waitForSelector('#pte-selection'), - page.waitForSelector('#pte-value'), - page.waitForSelector('#pte-revId'), - ]) - - if (!editableHandle || !selectionHandle || !valueHandle || !revIdHandle) { - throw new Error('Failed to find required editor elements') - } - - const waitForRevision = async (mutatingFunction?: () => Promise) => { - if (mutatingFunction) { - const currentRevId = await revIdHandle.evaluate((node) => - node instanceof HTMLElement && node.innerText - ? JSON.parse(node.innerText)?.revId - : null, - ) - await mutatingFunction() - await page.waitForSelector(`code[data-rev-id]:not([data-rev-id='${currentRevId}'])`, { - timeout: REVISION_TIMEOUT_MS, - }) - } - } - - const getSelection = async (): Promise => { - const selection = await selectionHandle.evaluate((node) => - node instanceof HTMLElement && node.innerText ? JSON.parse(node.innerText) : null, - ) - return selection - } - const waitForNewSelection = async (selectionChangeFn: () => Promise) => { - const oldSelection = await getSelection() - const dataVal = oldSelection ? JSON.stringify(oldSelection) : 'null' - await selectionChangeFn() - await page.waitForSelector(`code[data-selection]:not([data-selection='${dataVal}'])`, { - timeout: SELECTION_TIMEOUT_MS, - }) - } - - const waitForSelection = async (selection: EditorSelection) => { - if (selection && typeof selection.backward === 'undefined') { - selection.backward = false - } - const value = await valueHandle.evaluate((node): PortableTextBlock[] | undefined => - node instanceof HTMLElement && node.innerText - ? JSON.parse(node.innerText) - : undefined, - ) - const normalized = normalizeSelection(selection, value) - const dataVal = JSON.stringify(normalized) - await page.waitForSelector(`code[data-selection='${dataVal}']`, { - timeout: SELECTION_TIMEOUT_MS, - }) - } - return { - testId, - editorId, - insertText: async (text: string) => { - await editableHandle.focus() - await waitForRevision(async () => { - await editableHandle.evaluate( - (node, args) => { - node.dispatchEvent( - new InputEvent('beforeinput', { - bubbles: true, - cancelable: true, - inputType: 'insertText', - data: args[0], - }), - ) - }, - [text], - ) - }) - }, - undo: async () => { - await waitForRevision(async () => { - await editableHandle.focus() - await page.keyboard.down(metaKey) - await page.keyboard.press('z') - await page.keyboard.up(metaKey) - }) - }, - redo: async () => { - await waitForRevision(async () => { - await editableHandle.focus() - await page.keyboard.down(metaKey) - await page.keyboard.press('y') - await page.keyboard.up(metaKey) - }) - }, - paste: async (string: string, type = 'text/plain') => { - // Write text to native clipboard - await page.evaluate( - async ({string: _string, type: _type}) => { - await navigator.clipboard.writeText('') // Clear first - const blob = new Blob([_string], {type: _type}) - const data = [new ClipboardItem({[_type]: blob})] - await navigator.clipboard.write(data) - }, - {string, type}, - ) - await waitForRevision(async () => { - // Simulate paste key command - await page.keyboard.down(metaKey) - await page.keyboard.press('v') - await page.keyboard.up(metaKey) - }) - }, - pressKey: async (keyName: string, times?: number) => { - const pressKey = async () => { - await editableHandle.press(keyName) - } - for (let i = 0; i < (times || 1); i++) { - // Value manipulation keys - if ( - keyName.length === 1 || - keyName === 'Backspace' || - keyName === 'Delete' || - keyName === 'Enter' - ) { - await waitForRevision(async () => { - await pressKey() - }) - } else if ( - // Selection manipulation keys - [ - 'ArrowUp', - 'ArrowDown', - 'ArrowLeft', - 'ArrowRight', - 'PageUp', - 'PageDown', - 'Home', - 'End', - ].includes(keyName) - ) { - await waitForNewSelection(pressKey) - } else { - // Unknown keys, test needs should be covered by the above cases. - console.warn(`Key ${keyName} not accounted for`) - await pressKey() - } - } - }, - toggleMark: async (hotkey: string) => { - const selection = await selectionHandle.evaluate((node) => - node instanceof HTMLElement && node.innerText ? JSON.parse(node.innerText) : null, - ) - const performKeyPress = async () => { - await page.keyboard.down(metaKey) - await page.keyboard.down(hotkey) - - await page.keyboard.up(hotkey) - await page.keyboard.up(metaKey) - } - if (selection && isEqual(selection.focus, selection.anchor)) { - await performKeyPress() - } else { - await waitForRevision(performKeyPress) - } - }, - focus: async () => { - await editableHandle.focus() - }, - setSelection: async (selection: EditorSelection | null) => { - if (selection && typeof selection.backward === 'undefined') { - selection.backward = false - } - ipc.of.socketServer.emit( - 'payload', - JSON.stringify({ - type: 'selection', - selection, - testId, - editorId, - }), - ) - await waitForSelection(selection) - }, - async getValue(): Promise { - const value = await valueHandle.evaluate((node): PortableTextBlock[] | undefined => - node instanceof HTMLElement && node.innerText - ? JSON.parse(node.innerText) - : undefined, - ) - return value - }, - getSelection, - } - }), - ) - - // Open up the test documents - await this._pageA?.goto(`${WEB_SERVER_ROOT_URL}?editorId=A${testId}&testId=${testId}`, { - waitUntil: 'load', - }) - await this._pageB?.goto(`${WEB_SERVER_ROOT_URL}?editorId=B${testId}&testId=${testId}`, { - waitUntil: 'load', - }) - } -} diff --git a/packages/@sanity/portable-text-editor/e2e-tests/setup/globalSetup.ts b/packages/@sanity/portable-text-editor/e2e-tests/setup/globalSetup.ts deleted file mode 100644 index c4764ef4211..00000000000 --- a/packages/@sanity/portable-text-editor/e2e-tests/setup/globalSetup.ts +++ /dev/null @@ -1,25 +0,0 @@ -import path from 'node:path' - -import {setup as setupDevServer} from 'jest-dev-server' - -const testFolderPath = path.resolve(__dirname, '..') - -declare global { - // eslint-disable-next-line no-var - var servers: any[] // For the globalSetup and globalTeardown script -} - -export default async function globalSetup(): Promise { - globalThis.servers = await setupDevServer([ - { - command: `vite --port 3000 ${testFolderPath}/web-server`, - launchTimeout: 10000, - }, - { - command: `node -r esbuild-register ${testFolderPath}/ws-server`, - launchTimeout: 10000, - port: 3001, - debug: true, - }, - ]) -} diff --git a/packages/@sanity/portable-text-editor/e2e-tests/setup/globalTeardown.ts b/packages/@sanity/portable-text-editor/e2e-tests/setup/globalTeardown.ts deleted file mode 100644 index 2a8397333d6..00000000000 --- a/packages/@sanity/portable-text-editor/e2e-tests/setup/globalTeardown.ts +++ /dev/null @@ -1,5 +0,0 @@ -import {teardown as teardownDevServer} from 'jest-dev-server' - -export default async function globalTeardown(): Promise { - await teardownDevServer(globalThis.servers) -} diff --git a/packages/@sanity/portable-text-editor/e2e-tests/setup/globals.jest.ts b/packages/@sanity/portable-text-editor/e2e-tests/setup/globals.jest.ts deleted file mode 100644 index 4534df2e9a8..00000000000 --- a/packages/@sanity/portable-text-editor/e2e-tests/setup/globals.jest.ts +++ /dev/null @@ -1,27 +0,0 @@ -import {type PortableTextBlock} from '@sanity/types' - -import {type EditorSelection} from '../../src' - -export {} - -type Value = PortableTextBlock[] | undefined - -type Editor = { - editorId: string - focus: () => Promise - getSelection: () => Promise - getValue: () => Promise - insertText: (text: string) => Promise - paste: (text: string, type?: string) => Promise - pressKey: (keyName: string, times?: number) => Promise - redo: () => Promise - setSelection: (selection: EditorSelection | null) => Promise - testId: string - toggleMark: (hotkey: string) => Promise - undo: () => Promise -} - -declare global { - function getEditors(): Promise - function setDocumentValue(value: Value): Promise -} diff --git a/packages/@sanity/portable-text-editor/e2e-tests/tsconfig.json b/packages/@sanity/portable-text-editor/e2e-tests/tsconfig.json deleted file mode 100644 index dbd4b3596a4..00000000000 --- a/packages/@sanity/portable-text-editor/e2e-tests/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../tsconfig.json", - "ts-node": { - // these options are overrides used only by ts-node - "compilerOptions": { - "module": "commonjs" - } - }, - "compilerOptions": { - "target": "ESNext" - } -} diff --git a/packages/@sanity/portable-text-editor/e2e-tests/web-server/app.tsx b/packages/@sanity/portable-text-editor/e2e-tests/web-server/app.tsx deleted file mode 100644 index edde3e1083a..00000000000 --- a/packages/@sanity/portable-text-editor/e2e-tests/web-server/app.tsx +++ /dev/null @@ -1,120 +0,0 @@ -/* eslint-disable i18next/no-literal-string */ -import {type PortableTextBlock} from '@sanity/types' -import {Box, Card, Heading, Inline, Stack, studioTheme, Text, ThemeProvider} from '@sanity/ui' -import {useCallback, useMemo, useState} from 'react' -import {Subject} from 'rxjs' - -import {type EditorSelection, type Patch} from '../../src' -import {Editor} from './components/Editor' -import {Value} from './components/Value' - -export function App() { - const patches$ = useMemo( - () => - new Subject<{ - patches: Patch[] - snapshot: PortableTextBlock[] | undefined - }>(), - [], - ) - const [value, setValue] = useState(null) - const [revId, setRevId] = useState(undefined) - const [selection, setSelection] = useState(null) - const {editorId, testId} = useMemo(() => { - const params = new URLSearchParams(document.location.search) - return { - editorId: params.get('editorId') || (Math.random() + 1).toString(36).slice(7), - testId: params.get('testId') || 'noTestIdGiven', - } - }, []) - const webSocket = useMemo(() => { - const socket = new WebSocket( - `ws://${window.location.hostname}:3001/?editorId=${editorId}&testId=${testId}`, - ) - socket.addEventListener('open', () => { - socket.send(JSON.stringify({type: 'hello', editorId, testId})) - }) - socket.addEventListener('message', (message) => { - if (message.data && typeof message.data === 'string') { - const data = JSON.parse(message.data) - if (data.testId === testId) { - switch (data.type) { - case 'value': - setValue(data.value) - setRevId(data.revId) - break - case 'selection': - if (data.editorId === editorId && data.testId === testId) { - setSelection(data.selection) - } - break - case 'mutation': - if (data.testId === testId) { - patches$.next({ - patches: data.patches, - snapshot: data.snapshot, - }) - } - break - default: - // Nothing - } - } - } - }) - return socket - }, [editorId, patches$, testId]) - - const handleMutation = useCallback( - (patches: Patch[]) => { - if (webSocket) { - webSocket.send(JSON.stringify({type: 'mutation', patches, editorId, testId})) - } - }, - [editorId, testId, webSocket], - ) - return ( - - - - - - Test Document - - - - - - - editorId: {editorId} - - - - - testId: {testId} - - - - - - - - Editor - - - - - - - - - - - ) -} diff --git a/packages/@sanity/portable-text-editor/e2e-tests/web-server/components/Editor.tsx b/packages/@sanity/portable-text-editor/e2e-tests/web-server/components/Editor.tsx deleted file mode 100644 index 4f9e50a222a..00000000000 --- a/packages/@sanity/portable-text-editor/e2e-tests/web-server/components/Editor.tsx +++ /dev/null @@ -1,239 +0,0 @@ -/* eslint-disable i18next/no-literal-string */ -import {type PortableTextBlock} from '@sanity/types' -import {Box, Card, Code, Text} from '@sanity/ui' -import {useCallback, useEffect, useMemo, useRef, useState} from 'react' -import {type Subject} from 'rxjs' -import {styled} from 'styled-components' - -import { - type BlockDecoratorRenderProps, - type BlockListItemRenderProps, - type BlockRenderProps, - type BlockStyleRenderProps, - type EditorChange, - type EditorSelection, - type HotkeyOptions, - type Patch, - PortableTextEditable, - PortableTextEditor, - type RenderBlockFunction, - type RenderChildFunction, -} from '../../../src' -import {portableTextType} from '../../schema' -import {createKeyGenerator} from '../keyGenerator' - -export const HOTKEYS: HotkeyOptions = { - marks: { - 'mod+b': 'strong', - 'mod+i': 'em', - }, - custom: { - 'mod+-': (e, editor) => { - e.preventDefault() - PortableTextEditor.toggleList(editor, 'number') - }, - }, -} - -export const BlockObject = styled.div` - border: ${(props) => (props.focused ? '1px solid blue' : '1px solid transparent')}; - background: ${(props) => (props.selected ? '#eeeeff' : 'transparent')}; - padding: 2em; -` - -function getRandomColor() { - const letters = '0123456789ABCDEF' - let color = '#' - for (let i = 0; i < 6; i++) { - color += letters[Math.floor(Math.random() * 16)] - } - return color -} - -const renderPlaceholder = () => 'Type here!' - -export const Editor = ({ - value, - onMutation, - editorId, - patches$, - selection, -}: { - value: PortableTextBlock[] | undefined - onMutation: (mutatingPatches: Patch[]) => void - editorId: string - patches$: Subject<{ - patches: Patch[] - snapshot: PortableTextBlock[] | undefined - }> - selection: EditorSelection | null -}) => { - const [selectionValue, setSelectionValue] = useState(selection) - const selectionString = useMemo(() => JSON.stringify(selectionValue), [selectionValue]) - const editor = useRef(null) - const keyGenFn = useMemo(() => createKeyGenerator(editorId.slice(0, 1)), [editorId]) - const [isOffline, setIsOffline] = useState(!window.navigator.onLine) - const [readOnly, setReadOnly] = useState(false) - - const renderBlock: RenderBlockFunction = useCallback((props) => { - const {value: block, schemaType, children} = props - if (editor.current) { - const textType = editor.current.schemaTypes.block - // Text blocks - if (schemaType.name === textType.name) { - return ( - - {children} - - ) - } - // Object blocks - return ( - - - <>{JSON.stringify(block)} - - - ) - } - return children - }, []) - - const renderChild: RenderChildFunction = useCallback((props) => { - const {schemaType, children} = props - if (editor.current) { - const textType = editor.current.schemaTypes.span - // Text spans - if (schemaType.name === textType.name) { - return children - } - // Inline objects - } - return children - }, []) - - const renderDecorator = useCallback((props: BlockDecoratorRenderProps) => { - const {value: mark, children} = props - switch (mark) { - case 'strong': - return {children} - case 'em': - return {children} - case 'code': - return {children} - case 'underline': - return {children} - case 'strike-through': - return {children} - default: - return children - } - }, []) - - const renderStyle = useCallback((props: BlockStyleRenderProps) => { - return props.children - }, []) - - const handleChange = useCallback( - (change: EditorChange): void => { - switch (change.type) { - case 'selection': - setSelectionValue(change.selection) - break - case 'mutation': - onMutation(change.patches) - break - case 'connection': - if (change.value === 'offline') { - setIsOffline(true) - } else if (change.value === 'online') { - setIsOffline(false) - } - break - case 'blur': - case 'focus': - case 'invalidValue': - case 'loading': - case 'patch': - case 'ready': - case 'unset': - case 'value': - break - default: - throw new Error(`Unhandled editor change ${JSON.stringify(change)}`) - } - }, - [onMutation], - ) - - const renderListItem = useCallback((props: BlockListItemRenderProps) => { - const {level, schemaType, value: listType, children} = props - const listStyleType = schemaType.value === 'number' ? 'decimal' : 'inherit' - return
  • {children}
  • - }, []) - - const editable = useMemo( - () => ( - - ), - [renderBlock, renderChild, renderDecorator, renderListItem, renderStyle, selection], - ) - - // Make sure that the test editor is focused and out of "readOnly mode". - useEffect(() => { - if (editor.current) { - PortableTextEditor.focus(editor.current) - } - }, [editor]) - - const handleToggleReadOnly = useCallback(() => { - setReadOnly(!readOnly) - }, [readOnly]) - - if (!editorId) { - return null - } - - return ( - - - {editable} - - - - {selectionString} - - - - - - - ) -} diff --git a/packages/@sanity/portable-text-editor/e2e-tests/web-server/components/Value.tsx b/packages/@sanity/portable-text-editor/e2e-tests/web-server/components/Value.tsx deleted file mode 100644 index ebf1b2c28cc..00000000000 --- a/packages/@sanity/portable-text-editor/e2e-tests/web-server/components/Value.tsx +++ /dev/null @@ -1,27 +0,0 @@ -/* eslint-disable i18next/no-literal-string */ -import {type PortableTextBlock} from '@sanity/types' -import {Box, Card, Code, Heading} from '@sanity/ui' - -type Props = {value: PortableTextBlock[] | undefined; revId: string} - -export function Value({value, revId}: Props) { - return ( - - - - Value - - - - - {JSON.stringify(value, null, 2)} - - - - - {JSON.stringify({revId})} - - - - ) -} diff --git a/packages/@sanity/portable-text-editor/e2e-tests/web-server/entry.tsx b/packages/@sanity/portable-text-editor/e2e-tests/web-server/entry.tsx deleted file mode 100644 index 363bd146459..00000000000 --- a/packages/@sanity/portable-text-editor/e2e-tests/web-server/entry.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import {createRoot} from 'react-dom/client' - -import {App} from './app' - -const rootEl = document.getElementById('root') -if (!rootEl) { - throw new Error('Root element not found') -} - -const root = createRoot(rootEl) -root.render() diff --git a/packages/@sanity/portable-text-editor/e2e-tests/web-server/index.html b/packages/@sanity/portable-text-editor/e2e-tests/web-server/index.html deleted file mode 100644 index 75f4f6ff13d..00000000000 --- a/packages/@sanity/portable-text-editor/e2e-tests/web-server/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - Test Editor - - -
    - - - diff --git a/packages/@sanity/portable-text-editor/e2e-tests/web-server/keyGenerator.ts b/packages/@sanity/portable-text-editor/e2e-tests/web-server/keyGenerator.ts deleted file mode 100644 index b114154924f..00000000000 --- a/packages/@sanity/portable-text-editor/e2e-tests/web-server/keyGenerator.ts +++ /dev/null @@ -1,11 +0,0 @@ -// Example of custom keyGenerator - -const _createKeyGenerator = (prefix: string): (() => string) => { - let key = 0 - const fn = (): string => { - return `${prefix}-${key++}` - } - return fn -} - -export const createKeyGenerator = _createKeyGenerator diff --git a/packages/@sanity/portable-text-editor/e2e-tests/web-server/vite.config.js b/packages/@sanity/portable-text-editor/e2e-tests/web-server/vite.config.js deleted file mode 100644 index 9dfb0ec0296..00000000000 --- a/packages/@sanity/portable-text-editor/e2e-tests/web-server/vite.config.js +++ /dev/null @@ -1,24 +0,0 @@ -import path from 'node:path' - -import viteReact from '@vitejs/plugin-react' -import {defineConfig} from 'vite' - -// https://vitejs.dev/config/ -export default defineConfig({ - plugins: [viteReact()], - build: { - minify: false, - }, - server: { - host: true, - }, - resolve: { - alias: { - '@sanity/schema': path.join(__dirname, '../../../schema/src/legacy/Schema.ts'), - '@sanity/util/content': path.join(__dirname, '../../../util/src/content/index.ts'), - '@sanity/types': path.join(__dirname, '../../../types/src/index.ts'), - '@sanity/block-tools': path.join(__dirname, '../../../block-tools/src/index.ts'), - '@sanity/portable-text-editor': path.join(__dirname, '../../src/index.ts'), - }, - }, -}) diff --git a/packages/@sanity/portable-text-editor/e2e-tests/ws-server/index.ts b/packages/@sanity/portable-text-editor/e2e-tests/ws-server/index.ts deleted file mode 100644 index 2d68f1910e9..00000000000 --- a/packages/@sanity/portable-text-editor/e2e-tests/ws-server/index.ts +++ /dev/null @@ -1,127 +0,0 @@ -import {type PortableTextBlock} from '@sanity/types' -import express from 'express' -import expressWS from 'express-ws' -import ipc from 'node-ipc' -import {Subject} from 'rxjs' -import {type WebSocket} from 'ws' - -import {type Patch} from '../../src' -import {applyAll} from '../../src/patch/applyPatch' - -const WEBSOCKET_PORT = 3001 - -ipc.config.id = 'socketServer' -ipc.config.retry = 5000 -ipc.config.networkPort = 3002 -ipc.config.silent = true - -const expressApp = express() -const {app} = expressWS(expressApp) -const messages: Subject = new Subject() - -const valueMap: Record = {} -const revisionMap: Record = {} -const editorToSocket: Record = {} -const sockets: WebSocket[] = [] - -const sub = messages.subscribe((next) => { - sockets.forEach((socket) => { - const data = JSON.parse(next) - if (data.type === 'mutation') { - const isOriginator = editorToSocket[data.editorId] === socket - const patches = data.patches.map((p: Patch) => ({ - ...p, - origin: isOriginator ? 'local' : 'remote', - })) - const newData = JSON.stringify({ - ...data, - patches, - snapshot: data.snapshot, - }) - socket.send(newData) - return - } - socket.send(next) - }) -}) - -app.ws('/', (s, req) => { - const testId = req.query.testId?.toString() - if (testId && !sockets.includes(s)) { - sockets.push(s) - s.send( - JSON.stringify({ - type: 'value', - value: valueMap[testId], - testId, - revId: revisionMap[testId] || 'first', - }), - ) - } - s.on('close', () => { - const index = sockets.findIndex((socket) => socket === s) - if (index > -1) { - sockets.splice(index, 1) - } - }) - s.on('message', (msg: string) => { - const data = JSON.parse(msg) - let mutatedValue: PortableTextBlock[] | undefined | null = null - if (data.type === 'hello' && data.editorId) { - editorToSocket[data.editorId] = s - } - if (data.type === 'mutation' && testId) { - const prevValue = valueMap[testId] - try { - mutatedValue = applyAll(prevValue, data.patches) - messages.next(JSON.stringify(data)) - } catch (err) { - console.error(err) - // Nothing - } - if (mutatedValue !== null) { - // Assign revId and store value - const revId = (Math.random() + 1).toString(36).slice(7) - valueMap[testId] = mutatedValue - revisionMap[testId] = revId - // Broadcast to all - messages.next( - JSON.stringify({ - type: 'value', - value: mutatedValue, - testId, - revId, - }), - ) - } - } - }) -}) - -// Start the ipc server -ipc.serveNet(() => { - ipc.server.on('payload', (message) => { - const data = JSON.parse(message) - // Broadcast value and selection messages - // to set them in the clients - if (data.type === 'value') { - valueMap[data.testId] = data.value - revisionMap[data.testId] = data.revId - messages.next(message) - } - if (data.type === 'selection') { - messages.next(message) - } - }) -}) - -// Start the socket server -const server = app.listen(WEBSOCKET_PORT) -// Start the ipc server -ipc.server.start() - -process.on('SIGTERM', () => { - sub.unsubscribe() - ipc.server.stop() - server.close() -}) diff --git a/packages/@sanity/portable-text-editor/jest.config.cjs b/packages/@sanity/portable-text-editor/jest.config.cjs deleted file mode 100644 index eadf800d82a..00000000000 --- a/packages/@sanity/portable-text-editor/jest.config.cjs +++ /dev/null @@ -1,8 +0,0 @@ -'use strict' - -const {createJestConfig} = require('../../../test/config.cjs') - -module.exports = createJestConfig({ - displayName: require('./package.json').name, - modulePathIgnorePatterns: ['/e2e-tests'], -}) diff --git a/packages/@sanity/portable-text-editor/package.config.ts b/packages/@sanity/portable-text-editor/package.config.ts deleted file mode 100644 index c43051dd053..00000000000 --- a/packages/@sanity/portable-text-editor/package.config.ts +++ /dev/null @@ -1,4 +0,0 @@ -import baseConfig from '@repo/package.config' -import {defineConfig} from '@sanity/pkg-utils' - -export default defineConfig(baseConfig) diff --git a/packages/@sanity/portable-text-editor/package.json b/packages/@sanity/portable-text-editor/package.json deleted file mode 100644 index fd199b40902..00000000000 --- a/packages/@sanity/portable-text-editor/package.json +++ /dev/null @@ -1,114 +0,0 @@ -{ - "name": "@sanity/portable-text-editor", - "version": "3.48.1", - "description": "Portable Text Editor made in React", - "keywords": [ - "sanity", - "cms", - "headless", - "realtime", - "content", - "portable-text-editor", - "structure", - "api", - "collaborative", - "editor", - "text", - "portable-text" - ], - "homepage": "https://www.sanity.io/", - "bugs": { - "url": "https://github.com/sanity-io/sanity/issues" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/sanity-io/sanity.git", - "directory": "packages/@sanity/portable-text-editor" - }, - "license": "MIT", - "author": "Sanity.io ", - "sideEffects": false, - "exports": { - ".": { - "source": "./src/index.ts", - "import": "./lib/index.mjs", - "require": "./lib/index.js", - "default": "./lib/index.js" - }, - "./package.json": "./package.json" - }, - "main": "./lib/index.js", - "module": "./lib/index.esm.js", - "types": "./lib/index.d.ts", - "files": [ - "lib", - "src" - ], - "scripts": { - "build": "pkg-utils build --strict --check --clean", - "check:types": "tsc --project tsconfig.lib.json", - "clean": "rimraf lib", - "dev": "cd ./e2e-tests/ && tsx serve", - "lint": "eslint .", - "prepublishOnly": "turbo run build", - "prettier": "prettier --write './**/*.{ts,tsx,js,css,html}'", - "test": "jest", - "test:e2e": "jest --config=e2e-tests/e2e.config.cjs", - "test:watch": "jest --watch", - "watch": "pkg-utils watch" - }, - "dependencies": { - "@sanity/block-tools": "3.48.1", - "@sanity/schema": "3.48.1", - "@sanity/types": "3.48.1", - "@sanity/util": "3.48.1", - "debug": "^3.2.7", - "is-hotkey-esm": "^1.0.0", - "lodash": "^4.17.21", - "slate": "0.100.0", - "slate-react": "0.101.0" - }, - "devDependencies": { - "@jest/globals": "^29.7.0", - "@playwright/test": "1.41.2", - "@portabletext/toolkit": "^2.0.15", - "@repo/package.config": "workspace:*", - "@sanity/diff-match-patch": "^3.1.1", - "@sanity/ui": "^2.4.0", - "@testing-library/react": "^13.4.0", - "@types/debug": "^4.1.5", - "@types/express": "^4.17.21", - "@types/express-ws": "^3.0.1", - "@types/lodash": "^4.14.149", - "@types/node": "^18.19.8", - "@types/node-ipc": "^9.2.0", - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", - "@types/ws": "~8.5.3", - "@vitejs/plugin-react": "^4.3.1", - "express": "^4.18.3", - "express-ws": "^5.0.2", - "jest": "^29.7.0", - "jest-dev-server": "^9.0.1", - "jest-environment-node": "^29.7.0", - "node-ipc": "npm:@node-ipc/compat@9.2.5", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "rimraf": "^3.0.2", - "rxjs": "^7.8.1", - "styled-components": "^6.1.11", - "tsx": "^4.10.3", - "vite": "^4.5.3" - }, - "peerDependencies": { - "react": "^16.9 || ^17 || ^18", - "rxjs": "^7", - "styled-components": "^6.1" - }, - "engines": { - "node": ">=18" - }, - "publishConfig": { - "access": "public" - } -} diff --git a/packages/@sanity/portable-text-editor/src/editor/Editable.tsx b/packages/@sanity/portable-text-editor/src/editor/Editable.tsx deleted file mode 100644 index 04b9e6f07f5..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/Editable.tsx +++ /dev/null @@ -1,683 +0,0 @@ -import {type PortableTextBlock} from '@sanity/types' -import {isEqual, noop} from 'lodash' -import { - type ClipboardEvent, - type CSSProperties, - type FocusEventHandler, - type ForwardedRef, - forwardRef, - type HTMLProps, - type KeyboardEvent, - type MutableRefObject, - type ReactNode, - type TextareaHTMLAttributes, - useCallback, - useEffect, - useImperativeHandle, - useMemo, - useRef, - useState, -} from 'react' -import { - type BaseRange, - Editor, - Node, - type NodeEntry, - type Operation, - Path, - Range as SlateRange, - type Text, - Transforms, -} from 'slate' -import { - Editable as SlateEditable, - ReactEditor, - type RenderElementProps, - type RenderLeafProps, - useSlate, -} from 'slate-react' - -import { - type EditorChange, - type EditorSelection, - type OnCopyFn, - type OnPasteFn, - type OnPasteResult, - type RangeDecoration, - type RenderAnnotationFunction, - type RenderBlockFunction, - type RenderChildFunction, - type RenderDecoratorFunction, - type RenderListItemFunction, - type RenderStyleFunction, - type ScrollSelectionIntoViewFunction, -} from '../types/editor' -import {type HotkeyOptions} from '../types/options' -import {type SlateTextBlock, type VoidElement} from '../types/slate' -import {debugWithName} from '../utils/debug' -import {moveRangeByOperation, toPortableTextRange, toSlateRange} from '../utils/ranges' -import {normalizeSelection} from '../utils/selection' -import {fromSlateValue, isEqualToEmptyEditor, toSlateValue} from '../utils/values' -import {Element} from './components/Element' -import {Leaf} from './components/Leaf' -import {usePortableTextEditor} from './hooks/usePortableTextEditor' -import {usePortableTextEditorKeyGenerator} from './hooks/usePortableTextEditorKeyGenerator' -import {usePortableTextEditorReadOnlyStatus} from './hooks/usePortableTextReadOnly' -import {createWithHotkeys, createWithInsertData} from './plugins' -import {PortableTextEditor} from './PortableTextEditor' - -const debug = debugWithName('component:Editable') - -const PLACEHOLDER_STYLE: CSSProperties = { - position: 'absolute', - userSelect: 'none', - pointerEvents: 'none', - left: 0, - right: 0, -} - -interface BaseRangeWithDecoration extends BaseRange { - rangeDecoration: RangeDecoration -} - -const EMPTY_DECORATIONS_STATE: BaseRangeWithDecoration[] = [] - -/** - * @public - */ -export type PortableTextEditableProps = Omit< - TextareaHTMLAttributes, - 'onPaste' | 'onCopy' | 'onBeforeInput' -> & { - hotkeys?: HotkeyOptions - onBeforeInput?: (event: InputEvent) => void - onPaste?: OnPasteFn - onCopy?: OnCopyFn - ref: MutableRefObject - rangeDecorations?: RangeDecoration[] - renderAnnotation?: RenderAnnotationFunction - renderBlock?: RenderBlockFunction - renderChild?: RenderChildFunction - renderDecorator?: RenderDecoratorFunction - renderListItem?: RenderListItemFunction - renderPlaceholder?: () => ReactNode - renderStyle?: RenderStyleFunction - scrollSelectionIntoView?: ScrollSelectionIntoViewFunction - selection?: EditorSelection - spellCheck?: boolean -} - -/** - * @public - */ -export const PortableTextEditable = forwardRef(function PortableTextEditable( - props: PortableTextEditableProps & - Omit, 'as' | 'onPaste' | 'onBeforeInput'>, - forwardedRef: ForwardedRef, -) { - const { - hotkeys, - onBlur, - onFocus, - onBeforeInput, - onPaste, - onCopy, - onClick, - rangeDecorations, - renderAnnotation, - renderBlock, - renderChild, - renderDecorator, - renderListItem, - renderPlaceholder, - renderStyle, - selection: propsSelection, - scrollSelectionIntoView, - spellCheck, - ...restProps - } = props - - const portableTextEditor = usePortableTextEditor() - const readOnly = usePortableTextEditorReadOnlyStatus() - const keyGenerator = usePortableTextEditorKeyGenerator() - const ref = useRef(null) - const [editableElement, setEditableElement] = useState(null) - const [hasInvalidValue, setHasInvalidValue] = useState(false) - const [rangeDecorationState, setRangeDecorationsState] = - useState(EMPTY_DECORATIONS_STATE) - - // Forward ref to parent component - useImperativeHandle(forwardedRef, () => ref.current) - - const rangeDecorationsRef = useRef(rangeDecorations) - - const {change$, schemaTypes} = portableTextEditor - const slateEditor = useSlate() - - const blockTypeName = schemaTypes.block.name - - // React/UI-specific plugins - const withInsertData = useMemo( - () => createWithInsertData(change$, schemaTypes, keyGenerator), - [change$, keyGenerator, schemaTypes], - ) - const withHotKeys = useMemo( - () => createWithHotkeys(schemaTypes, portableTextEditor, hotkeys), - [hotkeys, portableTextEditor, schemaTypes], - ) - - // Output a minimal React editor inside Editable when in readOnly mode. - // NOTE: make sure all the plugins used here can be safely run over again at any point. - // There will be a problem if they redefine editor methods and then calling the original method within themselves. - useMemo(() => { - if (readOnly) { - debug('Editable is in read only mode') - return withInsertData(slateEditor) - } - debug('Editable is in edit mode') - return withInsertData(withHotKeys(slateEditor)) - }, [readOnly, slateEditor, withHotKeys, withInsertData]) - - const renderElement = useCallback( - (eProps: RenderElementProps) => ( - - ), - [schemaTypes, spellCheck, readOnly, renderBlock, renderChild, renderListItem, renderStyle], - ) - - const renderLeaf = useCallback( - ( - lProps: RenderLeafProps & { - leaf: Text & {placeholder?: boolean; rangeDecoration?: RangeDecoration} - }, - ) => { - if (lProps.leaf._type === 'span') { - let rendered = ( - - ) - if (renderPlaceholder && lProps.leaf.placeholder && lProps.text.text === '') { - return ( - <> - - {renderPlaceholder()} - - {rendered} - - ) - } - const decoration = lProps.leaf.rangeDecoration - if (decoration) { - rendered = decoration.component({children: rendered}) - } - return rendered - } - return lProps.children - }, - [readOnly, renderAnnotation, renderChild, renderDecorator, renderPlaceholder, schemaTypes], - ) - - const restoreSelectionFromProps = useCallback(() => { - if (propsSelection) { - debug(`Selection from props ${JSON.stringify(propsSelection)}`) - const normalizedSelection = normalizeSelection( - propsSelection, - fromSlateValue(slateEditor.children, blockTypeName), - ) - if (normalizedSelection !== null) { - debug(`Normalized selection from props ${JSON.stringify(normalizedSelection)}`) - const slateRange = toSlateRange(normalizedSelection, slateEditor) - if (slateRange) { - Transforms.select(slateEditor, slateRange) - // Output selection here in those cases where the editor selection was the same, and there are no set_selection operations made. - // The selection is usually automatically emitted to change$ by the withPortableTextSelections plugin whenever there is a set_selection operation applied. - if (!slateEditor.operations.some((o) => o.type === 'set_selection')) { - change$.next({type: 'selection', selection: normalizedSelection}) - } - slateEditor.onChange() - } - } - } - }, [propsSelection, slateEditor, blockTypeName, change$]) - - const syncRangeDecorations = useCallback( - (operation?: Operation) => { - if (rangeDecorations && rangeDecorations.length > 0) { - const newSlateRanges: BaseRangeWithDecoration[] = [] - rangeDecorations.forEach((rangeDecorationItem) => { - const slateRange = toSlateRange(rangeDecorationItem.selection, slateEditor) - if (!SlateRange.isRange(slateRange)) { - if (rangeDecorationItem.onMoved) { - rangeDecorationItem.onMoved({ - newSelection: null, - rangeDecoration: rangeDecorationItem, - origin: 'local', - }) - } - return - } - let newRange: BaseRange | null | undefined - if (operation) { - newRange = moveRangeByOperation(slateRange, operation) - if ((newRange && newRange !== slateRange) || (newRange === null && slateRange)) { - const value = PortableTextEditor.getValue(portableTextEditor) - const newRangeSelection = toPortableTextRange(value, newRange, schemaTypes) - if (rangeDecorationItem.onMoved) { - rangeDecorationItem.onMoved({ - newSelection: newRangeSelection, - rangeDecoration: rangeDecorationItem, - origin: 'local', - }) - } - } - } - // If the newRange is null, it means that the range is not valid anymore and should be removed - // If it's undefined, it means that the slateRange is still valid and should be kept - if (newRange !== null) { - newSlateRanges.push({...(newRange || slateRange), rangeDecoration: rangeDecorationItem}) - } - }) - if (newSlateRanges.length > 0) { - setRangeDecorationsState(newSlateRanges) - return - } - } - setRangeDecorationsState(EMPTY_DECORATIONS_STATE) - }, - [portableTextEditor, rangeDecorations, schemaTypes, slateEditor], - ) - - // Subscribe to change$ and restore selection from props when the editor has been initialized properly with it's value - useEffect(() => { - // debug('Subscribing to editor changes$') - const sub = change$.subscribe((next: EditorChange): void => { - switch (next.type) { - case 'ready': - restoreSelectionFromProps() - break - case 'invalidValue': - setHasInvalidValue(true) - break - case 'value': - setHasInvalidValue(false) - break - default: - } - }) - return () => { - // debug('Unsubscribing to changes$') - sub.unsubscribe() - } - }, [change$, restoreSelectionFromProps]) - - // Restore selection from props when it changes - useEffect(() => { - if (propsSelection && !hasInvalidValue) { - restoreSelectionFromProps() - } - }, [hasInvalidValue, propsSelection, restoreSelectionFromProps]) - - // Store reference to original apply function (see below for usage in useEffect) - const originalApply = useMemo(() => slateEditor.apply, [slateEditor]) - - const [syncedRangeDecorations, setSyncedRangeDecorations] = useState(false) - useEffect(() => { - if (!syncedRangeDecorations) { - // We only want this to run once, on mount - setSyncedRangeDecorations(true) - syncRangeDecorations() - } - }, [syncRangeDecorations, syncedRangeDecorations]) - - useEffect(() => { - if (!isEqual(rangeDecorations, rangeDecorationsRef.current)) { - syncRangeDecorations() - } - rangeDecorationsRef.current = rangeDecorations - }, [rangeDecorations, syncRangeDecorations]) - - // Sync range decorations after an operation is applied - useEffect(() => { - slateEditor.apply = (op: Operation) => { - originalApply(op) - if (op.type !== 'set_selection') { - syncRangeDecorations(op) - } - } - return () => { - slateEditor.apply = originalApply - } - }, [originalApply, slateEditor, syncRangeDecorations]) - - // Handle from props onCopy function - const handleCopy = useCallback( - (event: ClipboardEvent): void | ReactEditor => { - if (onCopy) { - const result = onCopy(event) - // CopyFn may return something to avoid doing default stuff - if (result !== undefined) { - event.preventDefault() - } - } - }, - [onCopy], - ) - - // Handle incoming pasting events in the editor - const handlePaste = useCallback( - (event: ClipboardEvent): Promise | void => { - event.preventDefault() - if (!slateEditor.selection) { - return - } - if (!onPaste) { - debug('Pasting normally') - slateEditor.insertData(event.clipboardData) - return - } - // Resolve it as promise (can be either async promise or sync return value) - new Promise((resolve) => { - const value = PortableTextEditor.getValue(portableTextEditor) - const ptRange = toPortableTextRange(value, slateEditor.selection, schemaTypes) - const path = ptRange?.focus.path || [] - resolve( - onPaste({ - event, - value, - path, - schemaTypes, - }), - ) - }) - .then((result) => { - debug('Custom paste function from client resolved', result) - change$.next({type: 'loading', isLoading: true}) - if (!result || !result.insert) { - debug('No result from custom paste handler, pasting normally') - slateEditor.insertData(event.clipboardData) - return - } - if (result && result.insert) { - slateEditor.insertFragment( - toSlateValue(result.insert as PortableTextBlock[], {schemaTypes}), - ) - change$.next({type: 'loading', isLoading: false}) - return - } - console.warn('Your onPaste function returned something unexpected:', result) - }) - .catch((error) => { - change$.next({type: 'loading', isLoading: false}) - console.error(error) // eslint-disable-line no-console - return error - }) - }, - [change$, onPaste, portableTextEditor, schemaTypes, slateEditor], - ) - - const handleOnFocus: FocusEventHandler = useCallback( - (event) => { - if (onFocus) { - onFocus(event) - } - if (!event.isDefaultPrevented()) { - const selection = PortableTextEditor.getSelection(portableTextEditor) - // Create an editor selection if it does'nt exist - if (selection === null) { - Transforms.select(slateEditor, Editor.start(slateEditor, [])) - slateEditor.onChange() - } - change$.next({type: 'focus', event}) - const newSelection = PortableTextEditor.getSelection(portableTextEditor) - // If the selection is the same, emit it explicitly here as there is no actual onChange event triggered. - if (selection === newSelection) { - change$.next({ - type: 'selection', - selection, - }) - } - } - }, - [onFocus, portableTextEditor, change$, slateEditor], - ) - - const handleClick = useCallback( - (event: React.MouseEvent) => { - if (onClick) { - onClick(event) - } - // Inserts a new block if it's clicking on the editor, focused on the last block and it's a void element - if (slateEditor.selection && event.target === event.currentTarget) { - const [lastBlock, path] = Node.last(slateEditor, []) - const focusPath = slateEditor.selection.focus.path.slice(0, 1) - const lastPath = path.slice(0, 1) - if (Path.equals(focusPath, lastPath)) { - const node = Node.descendant(slateEditor, path.slice(0, 1)) as - | SlateTextBlock - | VoidElement - if (lastBlock && Editor.isVoid(slateEditor, node)) { - Transforms.insertNodes(slateEditor, slateEditor.pteCreateEmptyBlock()) - slateEditor.onChange() - } - } - } - }, - [onClick, slateEditor], - ) - - const handleOnBlur: FocusEventHandler = useCallback( - (event) => { - if (onBlur) { - onBlur(event) - } - if (!event.isPropagationStopped()) { - change$.next({type: 'blur', event}) - } - }, - [change$, onBlur], - ) - - const handleOnBeforeInput = useCallback( - (event: InputEvent) => { - if (onBeforeInput) { - onBeforeInput(event) - } - }, - [onBeforeInput], - ) - - // This function will handle unexpected DOM changes inside the Editable rendering, - // and make sure that we can maintain a stable slateEditor.selection when that happens. - // - // For example, if this Editable is rendered inside something that might re-render - // this component (hidden contexts) while the user is still actively changing the - // contentEditable, this could interfere with the intermediate DOM selection, - // which again could be picked up by ReactEditor's event listeners. - // If that range is invalid at that point, the slate.editorSelection could be - // set either wrong, or invalid, to which slateEditor will throw exceptions - // that are impossible to recover properly from or result in a wrong selection. - // - // Also the other way around, when the ReactEditor will try to create a DOM Range - // from the current slateEditor.selection, it may throw unrecoverable errors - // if the current editor.selection is invalid according to the DOM. - // If this is the case, default to selecting the top of the document, if the - // user already had a selection. - const validateSelection = useCallback(() => { - if (!slateEditor.selection) { - return - } - const root = ReactEditor.findDocumentOrShadowRoot(slateEditor) - const {activeElement} = root - // Return if the editor isn't the active element - if (ref.current !== activeElement) { - return - } - const window = ReactEditor.getWindow(slateEditor) - const domSelection = window.getSelection() - if (!domSelection || domSelection.rangeCount === 0) { - return - } - const existingDOMRange = domSelection.getRangeAt(0) - try { - const newDOMRange = ReactEditor.toDOMRange(slateEditor, slateEditor.selection) - if ( - newDOMRange.startOffset !== existingDOMRange.startOffset || - newDOMRange.endOffset !== existingDOMRange.endOffset - ) { - debug('DOM range out of sync, validating selection') - // Remove all ranges temporary - domSelection?.removeAllRanges() - // Set the correct range - domSelection.addRange(newDOMRange) - } - } catch (error) { - debug(`Could not resolve selection, selecting top document`) - // Deselect the editor - Transforms.deselect(slateEditor) - // Select top document if there is a top block to select - if (slateEditor.children.length > 0) { - Transforms.select(slateEditor, [0, 0]) - } - slateEditor.onChange() - } - }, [ref, slateEditor]) - - // Observe mutations (child list and subtree) to this component's DOM, - // and make sure the editor selection is valid when that happens. - useEffect(() => { - if (editableElement) { - const mutationObserver = new MutationObserver(validateSelection) - mutationObserver.observe(editableElement, { - attributeOldValue: false, - attributes: false, - characterData: false, - childList: true, - subtree: true, - }) - return () => { - mutationObserver.disconnect() - } - } - return undefined - }, [validateSelection, editableElement]) - - const handleKeyDown = useCallback( - (event: KeyboardEvent) => { - if (props.onKeyDown) { - props.onKeyDown(event) - } - if (!event.isDefaultPrevented()) { - slateEditor.pteWithHotKeys(event) - } - }, - [props, slateEditor], - ) - - const scrollSelectionIntoViewToSlate = useMemo(() => { - // Use slate-react default scroll into view - if (scrollSelectionIntoView === undefined) { - return undefined - } - // Disable scroll into view totally - if (scrollSelectionIntoView === null) { - return noop - } - // Translate PortableTextEditor prop fn to Slate plugin fn - return (editor: ReactEditor, domRange: Range) => { - scrollSelectionIntoView(portableTextEditor, domRange) - } - }, [portableTextEditor, scrollSelectionIntoView]) - - const decorate: (entry: NodeEntry) => BaseRange[] = useCallback( - ([, path]) => { - if (isEqualToEmptyEditor(slateEditor.children, schemaTypes)) { - return [ - { - anchor: { - path: [0, 0], - offset: 0, - }, - focus: { - path: [0, 0], - offset: 0, - }, - placeholder: true, - }, - ] - } - // Editor node has a path length of 0 (should never be decorated) - if (path.length === 0) { - return EMPTY_DECORATIONS_STATE - } - const result = rangeDecorationState.filter((item) => { - // Special case in order to only return one decoration for collapsed ranges - if (SlateRange.isCollapsed(item)) { - // Collapsed ranges should only be decorated if they are on a block child level (length 2) - if (path.length !== 2) { - return false - } - return Path.equals(item.focus.path, path) && Path.equals(item.anchor.path, path) - } - // Include decorations that either include or intersects with this path - return ( - SlateRange.intersection(item, {anchor: {path, offset: 0}, focus: {path, offset: 0}}) || - SlateRange.includes(item, path) - ) - }) - if (result.length > 0) { - return result - } - return EMPTY_DECORATIONS_STATE - }, - [slateEditor, schemaTypes, rangeDecorationState], - ) - - // Set the forwarded ref to be the Slate editable DOM element - // Also set the editable element in a state so that the MutationObserver - // is setup when this element is ready. - useEffect(() => { - ref.current = ReactEditor.toDOMNode(slateEditor, slateEditor) as HTMLDivElement | null - setEditableElement(ref.current) - }, [slateEditor, ref]) - - if (!portableTextEditor) { - return null - } - return hasInvalidValue ? null : ( - - ) -}) diff --git a/packages/@sanity/portable-text-editor/src/editor/PortableTextEditor.tsx b/packages/@sanity/portable-text-editor/src/editor/PortableTextEditor.tsx deleted file mode 100644 index 30c6c3d87f6..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/PortableTextEditor.tsx +++ /dev/null @@ -1,308 +0,0 @@ -import { - type ArrayDefinition, - type ArraySchemaType, - type BlockSchemaType, - type ObjectSchemaType, - type Path, - type PortableTextBlock, - type PortableTextChild, - type PortableTextObject, - type SpanSchemaType, -} from '@sanity/types' -import {Component, type MutableRefObject, type PropsWithChildren} from 'react' -import {Subject} from 'rxjs' - -import { - type EditableAPI, - type EditableAPIDeleteOptions, - type EditorChange, - type EditorChanges, - type EditorSelection, - type PatchObservable, - type PortableTextMemberSchemaTypes, -} from '../types/editor' -import {debugWithName} from '../utils/debug' -import {getPortableTextMemberSchemaTypes} from '../utils/getPortableTextMemberSchemaTypes' -import {compileType} from '../utils/schema' -import {SlateContainer} from './components/SlateContainer' -import {Synchronizer} from './components/Synchronizer' -import {defaultKeyGenerator} from './hooks/usePortableTextEditorKeyGenerator' - -const debug = debugWithName('component:PortableTextEditor') - -/** - * Props for the PortableTextEditor component - * - * @public - */ -/** - * Props for the PortableTextEditor component - * - * @public - */ -export type PortableTextEditorProps = PropsWithChildren<{ - /** - * Function that gets called when the editor changes the value - */ - onChange: (change: EditorChange) => void - - /** - * Schema type for the portable text field - */ - schemaType: ArraySchemaType | ArrayDefinition - - /** - * Maximum number of blocks to allow within the editor - */ - maxBlocks?: number | string - - /** - * Whether or not the editor should be in read-only mode - */ - readOnly?: boolean - - /** - * The current value of the portable text field - */ - value?: PortableTextBlock[] - - /** - * Function used to generate keys for array items (`_key`) - */ - keyGenerator?: () => string - - /** - * Observable of local and remote patches for the edited value. - */ - patches$?: PatchObservable - - /** - * Backward compatibility (renamed to patches$). - */ - incomingPatches$?: PatchObservable - - /** - * A ref to the editor instance - */ - editorRef?: MutableRefObject -}> - -/** - * The main Portable Text Editor component. - * @public - */ -export class PortableTextEditor extends Component { - /** - * An observable of all the editor changes. - */ - public change$: EditorChanges = new Subject() - /** - * A lookup table for all the relevant schema types for this portable text type. - */ - public schemaTypes: PortableTextMemberSchemaTypes - /** - * The editor API (currently implemented with Slate). - */ - private editable?: EditableAPI - - constructor(props: PortableTextEditorProps) { - super(props) - - if (!props.schemaType) { - throw new Error('PortableTextEditor: missing "type" property') - } - - if (props.incomingPatches$) { - console.warn(`The prop 'incomingPatches$' is deprecated and renamed to 'patches$'`) - } - - this.change$.next({type: 'loading', isLoading: true}) - - this.schemaTypes = getPortableTextMemberSchemaTypes( - props.schemaType.hasOwnProperty('jsonType') - ? props.schemaType - : compileType(props.schemaType), - ) - } - - componentDidUpdate(prevProps: PortableTextEditorProps) { - // Set up the schema type lookup table again if the source schema type changes - if (this.props.schemaType !== prevProps.schemaType) { - this.schemaTypes = getPortableTextMemberSchemaTypes( - this.props.schemaType.hasOwnProperty('jsonType') - ? this.props.schemaType - : compileType(this.props.schemaType), - ) - } - if (this.props.editorRef !== prevProps.editorRef && this.props.editorRef) { - this.props.editorRef.current = this - } - } - - public setEditable = (editable: EditableAPI) => { - this.editable = {...this.editable, ...editable} - } - - render() { - const {onChange, value, children, patches$, incomingPatches$} = this.props - const {change$} = this - const _patches$ = incomingPatches$ || patches$ // Backward compatibility - - const maxBlocks = - typeof this.props.maxBlocks === 'undefined' - ? undefined - : parseInt(this.props.maxBlocks.toString(), 10) || undefined - - const readOnly = Boolean(this.props.readOnly) - const keyGenerator = this.props.keyGenerator || defaultKeyGenerator - return ( - - - {children} - - - ) - } - - // Static API methods - static activeAnnotations = (editor: PortableTextEditor): PortableTextObject[] => { - return editor && editor.editable ? editor.editable.activeAnnotations() : [] - } - static isAnnotationActive = ( - editor: PortableTextEditor, - annotationType: PortableTextObject['_type'], - ): boolean => { - return editor && editor.editable ? editor.editable.isAnnotationActive(annotationType) : false - } - static addAnnotation = ( - editor: PortableTextEditor, - type: ObjectSchemaType, - value?: {[prop: string]: unknown}, - ): {spanPath: Path; markDefPath: Path} | undefined => editor.editable?.addAnnotation(type, value) - static blur = (editor: PortableTextEditor): void => { - debug('Host blurred') - editor.editable?.blur() - } - static delete = ( - editor: PortableTextEditor, - selection: EditorSelection, - options?: EditableAPIDeleteOptions, - ) => editor.editable?.delete(selection, options) - static findDOMNode = ( - editor: PortableTextEditor, - element: PortableTextBlock | PortableTextChild, - ) => { - // eslint-disable-next-line react/no-find-dom-node - return editor.editable?.findDOMNode(element) - } - static findByPath = (editor: PortableTextEditor, path: Path) => { - return editor.editable?.findByPath(path) || [] - } - static focus = (editor: PortableTextEditor): void => { - debug('Host requesting focus') - editor.editable?.focus() - } - static focusBlock = (editor: PortableTextEditor) => { - return editor.editable?.focusBlock() - } - static focusChild = (editor: PortableTextEditor): PortableTextChild | undefined => { - return editor.editable?.focusChild() - } - static getSelection = (editor: PortableTextEditor) => { - return editor.editable ? editor.editable.getSelection() : null - } - static getValue = (editor: PortableTextEditor) => { - return editor.editable?.getValue() - } - static hasBlockStyle = (editor: PortableTextEditor, blockStyle: string) => { - return editor.editable?.hasBlockStyle(blockStyle) - } - static hasListStyle = (editor: PortableTextEditor, listStyle: string) => { - return editor.editable?.hasListStyle(listStyle) - } - static isCollapsedSelection = (editor: PortableTextEditor) => - editor.editable?.isCollapsedSelection() - static isExpandedSelection = (editor: PortableTextEditor) => - editor.editable?.isExpandedSelection() - static isMarkActive = (editor: PortableTextEditor, mark: string) => - editor.editable?.isMarkActive(mark) - static insertChild = ( - editor: PortableTextEditor, - type: SpanSchemaType | ObjectSchemaType, - value?: {[prop: string]: unknown}, - ): Path | undefined => { - debug(`Host inserting child`) - return editor.editable?.insertChild(type, value) - } - static insertBlock = ( - editor: PortableTextEditor, - type: BlockSchemaType | ObjectSchemaType, - value?: {[prop: string]: unknown}, - ): Path | undefined => { - return editor.editable?.insertBlock(type, value) - } - static insertBreak = (editor: PortableTextEditor): void => { - return editor.editable?.insertBreak() - } - static isVoid = (editor: PortableTextEditor, element: PortableTextBlock | PortableTextChild) => { - return editor.editable?.isVoid(element) - } - static isObjectPath = (editor: PortableTextEditor, path: Path): boolean => { - if (!path || !Array.isArray(path)) return false - const isChildObjectEditPath = path.length > 3 && path[1] === 'children' - const isBlockObjectEditPath = path.length > 1 && path[1] !== 'children' - return isBlockObjectEditPath || isChildObjectEditPath - } - static marks = (editor: PortableTextEditor) => { - return editor.editable?.marks() - } - static select = (editor: PortableTextEditor, selection: EditorSelection | null) => { - debug(`Host setting selection`, selection) - editor.editable?.select(selection) - } - static removeAnnotation = (editor: PortableTextEditor, type: ObjectSchemaType) => - editor.editable?.removeAnnotation(type) - static toggleBlockStyle = (editor: PortableTextEditor, blockStyle: string) => { - debug(`Host is toggling block style`) - return editor.editable?.toggleBlockStyle(blockStyle) - } - static toggleList = (editor: PortableTextEditor, listStyle: string): void => { - return editor.editable?.toggleList(listStyle) - } - static toggleMark = (editor: PortableTextEditor, mark: string): void => { - debug(`Host toggling mark`, mark) - editor.editable?.toggleMark(mark) - } - static getFragment = (editor: PortableTextEditor): PortableTextBlock[] | undefined => { - debug(`Host getting fragment`) - return editor.editable?.getFragment() - } - static undo = (editor: PortableTextEditor): void => { - debug('Host undoing') - editor.editable?.undo() - } - static redo = (editor: PortableTextEditor): void => { - debug('Host redoing') - editor.editable?.redo() - } - static isSelectionsOverlapping = ( - editor: PortableTextEditor, - selectionA: EditorSelection, - selectionB: EditorSelection, - ) => { - return editor.editable?.isSelectionsOverlapping(selectionA, selectionB) - } -} diff --git a/packages/@sanity/portable-text-editor/src/editor/__tests__/PortableTextEditor.test.tsx b/packages/@sanity/portable-text-editor/src/editor/__tests__/PortableTextEditor.test.tsx deleted file mode 100644 index 4419175d03f..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/__tests__/PortableTextEditor.test.tsx +++ /dev/null @@ -1,386 +0,0 @@ -import {describe, expect, it, jest} from '@jest/globals' -/* eslint-disable no-irregular-whitespace */ -import {type PortableTextBlock} from '@sanity/types' -import {render, waitFor} from '@testing-library/react' -import {createRef, type RefObject} from 'react' - -import {type EditorSelection} from '../..' -import {PortableTextEditor} from '../PortableTextEditor' -import {PortableTextEditorTester, schemaType} from './PortableTextEditorTester' - -const helloBlock: PortableTextBlock = { - _key: '123', - _type: 'myTestBlockType', - markDefs: [], - children: [{_key: '567', _type: 'span', text: 'Hello', marks: []}], -} - -const renderPlaceholder = () => 'Jot something down here' - -describe('initialization', () => { - it('receives initial onChange events and has custom placeholder', async () => { - const editorRef: RefObject = createRef() - const onChange = jest.fn() - const {container} = render( - , - ) - - await waitFor(() => { - expect(editorRef.current).not.toBe(null) - expect(onChange).toHaveBeenCalledWith({type: 'ready'}) - expect(onChange).toHaveBeenCalledWith({type: 'value', value: undefined}) - expect(container).toMatchInlineSnapshot(` -
    -
    -
    -
    -
    - - - Jot something down here - - - -  -
    -
    -
    -
    -
    -
    -
    -
    -
    -`) - }) - }) - it('takes value from props and confirms it by emitting value change event', async () => { - const initialValue = [helloBlock] - const onChange = jest.fn() - const editorRef = createRef() - render( - , - ) - const normalizedEditorValue = [{...initialValue[0], style: 'normal'}] - await waitFor(() => { - expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue}) - }) - if (editorRef.current) { - expect(PortableTextEditor.getValue(editorRef.current)).toStrictEqual(normalizedEditorValue) - } - }) - - it('takes initial selection from props', async () => { - const editorRef: RefObject = createRef() - const initialValue = [helloBlock] - const initialSelection: EditorSelection = { - anchor: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 2}, - focus: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 2}, - backward: false, - } - const onChange = jest.fn() - render( - , - ) - await waitFor(() => { - if (editorRef.current) { - PortableTextEditor.focus(editorRef.current) - expect(PortableTextEditor.getSelection(editorRef.current)).toStrictEqual(initialSelection) - } - }) - }) - - it('updates editor selection from new prop and keeps object equality in editor.getSelection()', async () => { - const editorRef: RefObject = createRef() - const initialValue = [helloBlock] - const initialSelection: EditorSelection = { - anchor: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 0}, - focus: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 0}, - backward: false, - } - const newSelection: EditorSelection = { - anchor: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 0}, - focus: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 3}, - backward: false, - } - const onChange = jest.fn() - const {rerender} = render( - , - ) - await waitFor(() => { - if (editorRef.current) { - expect(onChange).toHaveBeenCalledWith({type: 'ready'}) - expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue}) - const sel = PortableTextEditor.getSelection(editorRef.current) - PortableTextEditor.focus(editorRef.current) - - // Test for object equality here! - const anotherSel = PortableTextEditor.getSelection(editorRef.current) - expect(PortableTextEditor.getSelection(editorRef.current)).toStrictEqual(initialSelection) - expect(sel).toBe(anotherSel) - } - }) - rerender( - , - ) - waitFor(() => { - if (editorRef.current) { - expect(PortableTextEditor.getSelection(editorRef.current)).toEqual(newSelection) - } - }) - }) - - it('handles empty array value', async () => { - const editorRef: RefObject = createRef() - const initialValue: PortableTextBlock[] = [] - const initialSelection: EditorSelection = { - anchor: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 2}, - focus: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 2}, - } - const onChange = jest.fn() - render( - , - ) - await waitFor(() => { - if (editorRef.current) { - expect(onChange).not.toHaveBeenCalledWith({ - type: 'invalidValue', - value: initialValue, - resolution: { - action: 'Unset the value', - description: 'Editor value must be an array of Portable Text blocks, or undefined.', - item: initialValue, - patches: [ - { - path: [], - type: 'unset', - }, - ], - }, - }) - expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue}) - expect(onChange).toHaveBeenCalledWith({type: 'ready'}) - } - }) - }) - it('validates a non-initial value', async () => { - const editorRef: RefObject = createRef() - let value: PortableTextBlock[] = [helloBlock] - const initialSelection: EditorSelection = { - anchor: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 2}, - focus: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 2}, - } - const onChange = jest.fn() - let _rerender: any - await waitFor(() => { - render( - , - ) - _rerender = render - }) - await waitFor(() => { - expect(onChange).not.toHaveBeenCalledWith({ - type: 'invalidValue', - value, - resolution: { - action: 'Unset the value', - description: 'Editor value must be an array of Portable Text blocks, or undefined.', - item: value, - patches: [ - { - path: [], - type: 'unset', - }, - ], - }, - }) - expect(onChange).toHaveBeenCalledWith({type: 'value', value}) - }) - value = [{_type: 'banana', _key: '123'}] - const newOnChange = jest.fn() - _rerender( - , - ) - await waitFor(() => { - expect(newOnChange).toHaveBeenCalledWith({ - type: 'invalidValue', - value, - resolution: { - action: 'Remove the block', - description: "Block with _key '123' has invalid _type 'banana'", - item: value[0], - patches: [ - { - path: [{_key: '123'}], - type: 'unset', - }, - ], - i18n: { - action: 'inputs.portable-text.invalid-value.disallowed-type.action', - description: 'inputs.portable-text.invalid-value.disallowed-type.description', - values: { - key: '123', - typeName: 'banana', - }, - }, - }, - }) - }) - }) - it("doesn't crash when containing a invalid block somewhere inside the content", async () => { - const editorRef: RefObject = createRef() - const initialValue: PortableTextBlock[] = [ - helloBlock, - { - _key: 'abc', - _type: 'myTestBlockType', - markDefs: [], - children: [{_key: 'def', _type: 'span', marks: []}], - }, - ] - const initialSelection: EditorSelection = { - anchor: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 2}, - focus: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 2}, - } - const onChange = jest.fn() - render( - , - ) - await waitFor(() => { - if (editorRef.current) { - expect(onChange).toHaveBeenCalledWith({ - type: 'invalidValue', - value: initialValue, - resolution: { - action: 'Write an empty text property to the object', - description: - "Child with _key 'def' in block with key 'abc' has missing or invalid text property!", - i18n: { - action: 'inputs.portable-text.invalid-value.invalid-span-text.action', - description: 'inputs.portable-text.invalid-value.invalid-span-text.description', - values: { - key: 'abc', - childKey: 'def', - }, - }, - item: { - _key: 'abc', - _type: 'myTestBlockType', - children: [ - { - _key: 'def', - _type: 'span', - marks: [], - }, - ], - markDefs: [], - }, - patches: [ - { - path: [ - { - _key: 'abc', - }, - 'children', - { - _key: 'def', - }, - ], - type: 'set', - value: { - _key: 'def', - _type: 'span', - marks: [], - text: '', - }, - }, - ], - }, - }) - } - }) - expect(onChange).not.toHaveBeenCalledWith({type: 'value', value: initialValue}) - expect(onChange).toHaveBeenCalledWith({type: 'ready'}) - }) -}) diff --git a/packages/@sanity/portable-text-editor/src/editor/__tests__/PortableTextEditorTester.tsx b/packages/@sanity/portable-text-editor/src/editor/__tests__/PortableTextEditorTester.tsx deleted file mode 100644 index 5bb99728a2b..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/__tests__/PortableTextEditorTester.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import {jest} from '@jest/globals' -import {Schema} from '@sanity/schema' -import {defineArrayMember, defineField} from '@sanity/types' -import {type ForwardedRef, forwardRef, useCallback, useEffect, useMemo} from 'react' - -import { - PortableTextEditable, - type PortableTextEditableProps, - PortableTextEditor, - type PortableTextEditorProps, -} from '../../index' - -const imageType = defineField({ - type: 'image', - name: 'blockImage', -}) - -const someObject = defineField({ - type: 'object', - name: 'someObject', - fields: [{type: 'string', name: 'color'}], -}) - -const blockType = defineField({ - type: 'block', - name: 'myTestBlockType', - styles: [ - {title: 'Normal', value: 'normal'}, - {title: 'H1', value: 'h1'}, - {title: 'H2', value: 'h2'}, - {title: 'H3', value: 'h3'}, - {title: 'H4', value: 'h4'}, - {title: 'H5', value: 'h5'}, - {title: 'H6', value: 'h6'}, - {title: 'Quote', value: 'blockquote'}, - ], - of: [someObject, imageType], -}) - -const portableTextType = defineArrayMember({ - type: 'array', - name: 'body', - of: [blockType, someObject], -}) - -const colorAndLink = defineArrayMember({ - type: 'array', - name: 'colorAndLink', - of: [ - { - ...blockType, - marks: { - annotations: [ - { - name: 'link', - type: 'object', - fields: [{type: 'string', name: 'color'}], - }, - { - name: 'color', - type: 'object', - fields: [{type: 'string', name: 'color'}], - }, - ], - }, - }, - ], -}) - -const schema = Schema.compile({ - name: 'test', - types: [portableTextType, colorAndLink], -}) - -let key = 0 - -export const PortableTextEditorTester = forwardRef(function PortableTextEditorTester( - props: Partial> & { - onChange?: PortableTextEditorProps['onChange'] - rangeDecorations?: PortableTextEditableProps['rangeDecorations'] - renderPlaceholder?: PortableTextEditableProps['renderPlaceholder'] - schemaType: PortableTextEditorProps['schemaType'] - selection?: PortableTextEditableProps['selection'] - value?: PortableTextEditorProps['value'] - }, - ref: ForwardedRef, -) { - useEffect(() => { - key = 0 - }, []) - const _keyGenerator = useCallback(() => { - key++ - return `${key}` - }, []) - const onChange = useMemo(() => props.onChange || jest.fn(), [props.onChange]) - return ( - - - - ) -}) - -export const schemaType = schema.get('body') - -export const schemaTypeWithColorAndLink = schema.get('colorAndLink') diff --git a/packages/@sanity/portable-text-editor/src/editor/__tests__/RangeDecorations.test.tsx b/packages/@sanity/portable-text-editor/src/editor/__tests__/RangeDecorations.test.tsx deleted file mode 100644 index 754d764354c..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/__tests__/RangeDecorations.test.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import {describe, expect, it, jest} from '@jest/globals' -/* eslint-disable no-irregular-whitespace */ -import {type PortableTextBlock} from '@sanity/types' -import {render, waitFor} from '@testing-library/react' -import {createRef, type ReactNode, type RefObject} from 'react' - -import {type RangeDecoration} from '../..' -import {type PortableTextEditor} from '../PortableTextEditor' -import {PortableTextEditorTester, schemaType} from './PortableTextEditorTester' - -const helloBlock: PortableTextBlock = { - _key: '123', - _type: 'myTestBlockType', - markDefs: [], - children: [{_key: '567', _type: 'span', text: 'Hello', marks: []}], -} - -let rangeDecorationIteration = 0 - -const RangeDecorationTestComponent = ({children}: {children?: ReactNode}) => { - rangeDecorationIteration++ - return {children} -} - -describe('RangeDecorations', () => { - it.only('only render range decorations as necessary', async () => { - const editorRef: RefObject = createRef() - const onChange = jest.fn() - const value = [helloBlock] - let rangeDecorations: RangeDecoration[] = [ - { - component: RangeDecorationTestComponent, - selection: { - anchor: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 0}, - focus: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 2}, - }, - }, - ] - const {rerender} = render( - , - ) - await waitFor(() => { - expect([rangeDecorationIteration, 'initial']).toEqual([0, 'initial']) - }) - // Re-render with the same range decorations - rerender( - , - ) - await waitFor(() => { - expect([rangeDecorationIteration, 'initial']).toEqual([0, 'initial']) - }) - // Update the range decorations, a new object with identical values - rangeDecorations = [ - { - component: RangeDecorationTestComponent, - selection: { - anchor: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 0}, - focus: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 2}, - }, - }, - ] - rerender( - , - ) - await waitFor(() => { - expect([rangeDecorationIteration, 'updated-with-equal-values']).toEqual([ - 0, - 'updated-with-equal-values', - ]) - }) - // Update the range decorations with a new offset - rangeDecorations = [ - { - component: RangeDecorationTestComponent, - selection: { - anchor: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 2}, - focus: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 4}, - }, - }, - ] - rerender( - , - ) - await waitFor(() => { - expect([rangeDecorationIteration, 'updated-with-different']).toEqual([ - 1, - 'updated-with-different', - ]) - }) - }) -}) diff --git a/packages/@sanity/portable-text-editor/src/editor/__tests__/handleClick.test.tsx b/packages/@sanity/portable-text-editor/src/editor/__tests__/handleClick.test.tsx deleted file mode 100644 index 92df75376fe..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/__tests__/handleClick.test.tsx +++ /dev/null @@ -1,218 +0,0 @@ -import {describe, expect, it, jest} from '@jest/globals' -import {fireEvent, render, waitFor} from '@testing-library/react' -import {createRef, type RefObject} from 'react' - -import {PortableTextEditor} from '../PortableTextEditor' -import {PortableTextEditorTester, schemaType} from './PortableTextEditorTester' -import {getEditableElement} from './utils' - -describe('adds empty text block if its needed', () => { - const newBlock = { - _type: 'myTestBlockType', - _key: '3', - style: 'normal', - markDefs: [], - children: [ - { - _type: 'span', - _key: '2', - text: '', - marks: [], - }, - ], - } - it('adds a new block at the bottom, when clicking on the portable text editor, because the only block is void and user is focused on that one', async () => { - const initialValue = [ - { - _key: 'b', - _type: 'someObject', - }, - ] - - const initialSelection = { - focus: {path: [{_key: 'b'}], offset: 0}, - anchor: {path: [{_key: 'b'}], offset: 0}, - } - - const editorRef: RefObject = createRef() - const onChange = jest.fn() - const component = render( - , - ) - const element = await getEditableElement(component) - - const editor = editorRef.current - const inlineType = editor?.schemaTypes.inlineObjects.find((t) => t.name === 'someObject') - await waitFor(async () => { - if (editor && inlineType && element) { - PortableTextEditor.focus(editor) - PortableTextEditor.select(editor, initialSelection) - fireEvent.click(element) - expect(PortableTextEditor.getValue(editor)).toEqual([initialValue[0], newBlock]) - } - }) - }) - it('should not add blocks if the last element is a text block', async () => { - const initialValue = [ - { - _key: 'b', - _type: 'someObject', - }, - { - _type: 'myTestBlockType', - _key: '3', - style: 'normal', - markDefs: [], - children: [ - { - _type: 'span', - _key: '2', - text: '', - marks: [], - }, - ], - }, - ] - - const initialSelection = { - focus: {path: [{_key: 'b'}], offset: 0}, - anchor: {path: [{_key: 'b'}], offset: 0}, - } - - const editorRef: RefObject = createRef() - const onChange = jest.fn() - const component = render( - , - ) - const element = await getEditableElement(component) - - const editor = editorRef.current - const inlineType = editor?.schemaTypes.inlineObjects.find((t) => t.name === 'someObject') - await waitFor(async () => { - if (editor && inlineType && element) { - PortableTextEditor.focus(editor) - PortableTextEditor.select(editor, initialSelection) - fireEvent.click(element) - expect(PortableTextEditor.getValue(editor)).toEqual(initialValue) - } - }) - }) - it('should not add blocks if the last element is void, but its not focused on that one', async () => { - const initialValue = [ - { - _key: 'a', - _type: 'someObject', - }, - { - _type: 'myTestBlockType', - _key: 'b', - style: 'normal', - markDefs: [], - children: [ - { - _type: 'span', - _key: 'b1', - text: '', - marks: [], - }, - ], - }, - { - _key: 'c', - _type: 'someObject', - }, - ] - - const initialSelection = { - focus: {path: [{_key: 'b'}, 'children', {_key: 'b1'}], offset: 2}, - anchor: {path: [{_key: 'b'}, 'children', {_key: 'b1'}], offset: 2}, - } - - const editorRef: RefObject = createRef() - const onChange = jest.fn() - const component = render( - , - ) - const element = await getEditableElement(component) - - const editor = editorRef.current - const inlineType = editor?.schemaTypes.inlineObjects.find((t) => t.name === 'someObject') - await waitFor(async () => { - if (editor && inlineType && element) { - PortableTextEditor.focus(editor) - PortableTextEditor.select(editor, initialSelection) - fireEvent.click(element) - expect(PortableTextEditor.getValue(editor)).toEqual(initialValue) - } - }) - }) - it('should not add blocks if the last element is void, and its focused on that one when clicking', async () => { - const initialValue = [ - { - _key: 'a', - _type: 'someObject', - }, - { - _type: 'myTestBlockType', - _key: 'b', - style: 'normal', - markDefs: [], - children: [ - { - _type: 'span', - _key: 'b1', - text: '', - marks: [], - }, - ], - }, - { - _key: 'c', - _type: 'someObject', - }, - ] - - const initialSelection = { - focus: {path: [{_key: 'c'}], offset: 0}, - anchor: {path: [{_key: 'c'}], offset: 0}, - } - - const editorRef: RefObject = createRef() - const onChange = jest.fn() - const component = render( - , - ) - const element = await getEditableElement(component) - - const editor = editorRef.current - const inlineType = editor?.schemaTypes.inlineObjects.find((t) => t.name === 'someObject') - await waitFor(async () => { - if (editor && inlineType && element) { - PortableTextEditor.focus(editor) - PortableTextEditor.select(editor, initialSelection) - fireEvent.click(element) - expect(PortableTextEditor.getValue(editor)).toEqual(initialValue.concat(newBlock)) - } - }) - }) -}) diff --git a/packages/@sanity/portable-text-editor/src/editor/__tests__/pteWarningsSelfSolving.test.tsx b/packages/@sanity/portable-text-editor/src/editor/__tests__/pteWarningsSelfSolving.test.tsx deleted file mode 100644 index 6a4a886130e..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/__tests__/pteWarningsSelfSolving.test.tsx +++ /dev/null @@ -1,389 +0,0 @@ -import {describe, expect, it, jest} from '@jest/globals' -import {type PortableTextBlock} from '@sanity/types' -import {render, waitFor} from '@testing-library/react' -import {createRef, type RefObject} from 'react' - -import {PortableTextEditor} from '../PortableTextEditor' -import {PortableTextEditorTester, schemaType} from './PortableTextEditorTester' - -describe('when PTE would display warnings, instead it self solves', () => { - it('when child at index is missing required _key in block with _key', async () => { - const editorRef: RefObject = createRef() - const initialValue = [ - { - _key: 'abc', - _type: 'myTestBlockType', - children: [ - { - _type: 'span', - marks: [], - text: 'Hello with a new key', - }, - ], - markDefs: [], - style: 'normal', - }, - ] - - const onChange = jest.fn() - render( - , - ) - await waitFor(() => { - expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue}) - expect(onChange).toHaveBeenCalledWith({type: 'ready'}) - }) - await waitFor(() => { - if (editorRef.current) { - expect(PortableTextEditor.getValue(editorRef.current)).toEqual([ - { - _key: 'abc', - _type: 'myTestBlockType', - children: [ - { - _key: '4', - _type: 'span', - text: 'Hello with a new key', - marks: [], - }, - ], - markDefs: [], - style: 'normal', - }, - ]) - } - }) - }) - - it('allows missing .markDefs', async () => { - const editorRef: RefObject = createRef() - const initialValue = [ - { - _key: 'abc', - _type: 'myTestBlockType', - children: [ - { - _key: 'def', - _type: 'span', - marks: [], - text: 'No markDefs', - }, - ], - style: 'normal', - }, - ] - - const onChange = jest.fn() - render( - , - ) - await waitFor(() => { - expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue}) - expect(onChange).toHaveBeenCalledWith({type: 'ready'}) - }) - await waitFor(() => { - if (editorRef.current) { - PortableTextEditor.focus(editorRef.current) - expect(PortableTextEditor.getValue(editorRef.current)).toEqual([ - { - _key: 'abc', - _type: 'myTestBlockType', - children: [ - { - _key: 'def', - _type: 'span', - text: 'No markDefs', - marks: [], - }, - ], - style: 'normal', - }, - ]) - } - }) - }) - - it('adds missing .children', async () => { - const editorRef: RefObject = createRef() - const initialValue = [ - { - _key: 'abc', - _type: 'myTestBlockType', - style: 'normal', - markDefs: [], - }, - { - _key: 'def', - _type: 'myTestBlockType', - style: 'normal', - children: [], - markDefs: [], - }, - ] - - const onChange = jest.fn() - render( - , - ) - await waitFor(() => { - expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue}) - expect(onChange).toHaveBeenCalledWith({type: 'ready'}) - }) - await waitFor(() => { - if (editorRef.current) { - PortableTextEditor.focus(editorRef.current) - expect(PortableTextEditor.getValue(editorRef.current)).toEqual([ - { - _key: 'abc', - _type: 'myTestBlockType', - children: [ - { - _key: '5', - _type: 'span', - text: '', - marks: [], - }, - ], - markDefs: [], - style: 'normal', - }, - { - _key: 'def', - _type: 'myTestBlockType', - children: [ - { - _key: '6', - _type: 'span', - text: '', - marks: [], - }, - ], - markDefs: [], - style: 'normal', - }, - ]) - } - }) - }) - - it('removes orphaned marks', async () => { - const editorRef: RefObject = createRef() - const initialValue = [ - { - _key: 'abc', - _type: 'myTestBlockType', - style: 'normal', - markDefs: [], - children: [ - { - _key: 'def', - _type: 'span', - marks: ['ghi'], - text: 'Hello', - }, - ], - }, - ] - - const onChange = jest.fn() - render( - , - ) - await waitFor(() => { - expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue}) - expect(onChange).toHaveBeenCalledWith({type: 'ready'}) - }) - await waitFor(() => { - if (editorRef.current) { - PortableTextEditor.focus(editorRef.current) - expect(PortableTextEditor.getValue(editorRef.current)).toEqual([ - { - _key: 'abc', - _type: 'myTestBlockType', - children: [ - { - _key: 'def', - _type: 'span', - text: 'Hello', - marks: [], - }, - ], - markDefs: [], - style: 'normal', - }, - ]) - } - }) - }) - - it('removes orphaned marksDefs', async () => { - const editorRef: RefObject = createRef() - const initialValue = [ - { - _key: 'abc', - _type: 'myTestBlockType', - style: 'normal', - markDefs: [ - { - _key: 'ghi', - _type: 'link', - href: 'https://sanity.io', - }, - ], - children: [ - { - _key: 'def', - _type: 'span', - marks: [], - text: 'Hello', - }, - ], - }, - ] - - const onChange = jest.fn() - render( - , - ) - await waitFor(() => { - expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue}) - expect(onChange).toHaveBeenCalledWith({type: 'ready'}) - }) - await waitFor(() => { - if (editorRef.current) { - PortableTextEditor.focus(editorRef.current) - expect(PortableTextEditor.getValue(editorRef.current)).toEqual([ - { - _key: 'abc', - _type: 'myTestBlockType', - children: [ - { - _key: 'def', - _type: 'span', - text: 'Hello', - marks: [], - }, - ], - markDefs: [], - style: 'normal', - }, - ]) - } - }) - }) - - it('allows missing .markDefs', async () => { - const editorRef: RefObject = createRef() - const initialValue = [ - { - _key: 'abc', - _type: 'myTestBlockType', - children: [ - { - _key: 'def', - _type: 'span', - marks: [], - text: 'No markDefs', - }, - ], - style: 'normal', - }, - ] - - const onChange = jest.fn() - render( - , - ) - await waitFor(() => { - expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue}) - expect(onChange).toHaveBeenCalledWith({type: 'ready'}) - }) - await waitFor(() => { - if (editorRef.current) { - PortableTextEditor.focus(editorRef.current) - expect(PortableTextEditor.getValue(editorRef.current)).toEqual([ - { - _key: 'abc', - _type: 'myTestBlockType', - children: [ - { - _key: 'def', - _type: 'span', - text: 'No markDefs', - marks: [], - }, - ], - style: 'normal', - }, - ]) - } - }) - }) - - it('allows empty array of blocks', async () => { - const editorRef: RefObject = createRef() - const initialValue = [] as PortableTextBlock[] - - const onChange = jest.fn() - render( - , - ) - await waitFor(() => { - expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue}) - expect(onChange).toHaveBeenCalledWith({type: 'ready'}) - }) - await waitFor(() => { - if (editorRef.current) { - PortableTextEditor.focus(editorRef.current) - expect(PortableTextEditor.getValue(editorRef.current)).toEqual([ - { - _key: '5', - _type: 'myTestBlockType', - children: [{_key: '4', _type: 'span', marks: [], text: ''}], - markDefs: [], - style: 'normal', - }, - ]) - } - }) - await waitFor(() => { - expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue}) - expect(onChange).toHaveBeenCalledWith({type: 'ready'}) - }) - }) -}) diff --git a/packages/@sanity/portable-text-editor/src/editor/__tests__/utils.ts b/packages/@sanity/portable-text-editor/src/editor/__tests__/utils.ts deleted file mode 100644 index ba469f060a8..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/__tests__/utils.ts +++ /dev/null @@ -1,39 +0,0 @@ -// This utils are inspired from https://github.dev/mwood23/slate-test-utils/blob/master/src/buildTestHarness.tsx -import {act, fireEvent, type render} from '@testing-library/react' -import {parseHotkey} from 'is-hotkey-esm' - -export async function triggerKeyboardEvent(hotkey: string, element: Element): Promise { - return act(async () => { - const eventProps = parseHotkey(hotkey) - const values = hotkey.split('+') - - fireEvent( - element, - new window.KeyboardEvent('keydown', { - key: values[values.length - 1], - code: `${eventProps.which}`, - keyCode: eventProps.which, - bubbles: true, - ...eventProps, - }), - ) - }) -} - -export async function getEditableElement(component: ReturnType): Promise { - await act(async () => component) - const element = component.container.querySelector('[data-slate-editor="true"]') - if (!element) { - throw new Error('Could not find element') - } - /** - * Manually add this because JSDom doesn't implement this and Slate checks for it - * internally before doing stuff. - * - * https://github.com/jsdom/jsdom/issues/1670 - */ - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - element.isContentEditable = true - return element -} diff --git a/packages/@sanity/portable-text-editor/src/editor/components/DraggableBlock.tsx b/packages/@sanity/portable-text-editor/src/editor/components/DraggableBlock.tsx deleted file mode 100644 index f614bc1cc2f..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/components/DraggableBlock.tsx +++ /dev/null @@ -1,287 +0,0 @@ -import { - type DragEvent, - type MutableRefObject, - type ReactNode, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react' -import {Editor, type Element as SlateElement, Path, Transforms} from 'slate' -import {ReactEditor, useSlateStatic} from 'slate-react' - -import {debugWithName} from '../../utils/debug' -import { - IS_DRAGGING, - IS_DRAGGING_BLOCK_ELEMENT, - IS_DRAGGING_BLOCK_TARGET_POSITION, - IS_DRAGGING_ELEMENT_TARGET, -} from '../../utils/weakMaps' - -const debug = debugWithName('components:DraggableBlock') -const debugRenders = false - -/** - * @internal - */ -export interface DraggableBlockProps { - children: ReactNode - element: SlateElement - readOnly: boolean - blockRef: MutableRefObject -} - -/** - * Implements drag and drop functionality on editor block nodes - * @internal - */ -export const DraggableBlock = ({children, element, readOnly, blockRef}: DraggableBlockProps) => { - const editor = useSlateStatic() - const dragGhostRef: MutableRefObject = useRef() - const [isDragOver, setIsDragOver] = useState(false) - const isVoid = useMemo(() => Editor.isVoid(editor, element), [editor, element]) - const isInline = useMemo(() => Editor.isInline(editor, element), [editor, element]) - - const [blockElement, setBlockElement] = useState(null) - - useEffect( - () => setBlockElement(blockRef ? blockRef.current : ReactEditor.toDOMNode(editor, element)), - [editor, element, blockRef], - ) - - // Note: this is called not for the dragging block, but for the targets when the block is dragged over them - const handleDragOver = useCallback( - (event: DragEvent) => { - const isMyDragOver = IS_DRAGGING_BLOCK_ELEMENT.get(editor) - // debug('Drag over', blockElement) - if (!isMyDragOver || !blockElement) { - return - } - event.preventDefault() - event.dataTransfer.dropEffect = 'move' - IS_DRAGGING_ELEMENT_TARGET.set(editor, element) - const elementRect = blockElement.getBoundingClientRect() - const offset = elementRect.top - const height = elementRect.height - const Y = event.pageY - const loc = Math.abs(offset - Y) - let position: 'top' | 'bottom' = 'bottom' - if (element === editor.children[0]) { - position = 'top' - } else if (loc < height / 2) { - position = 'top' - IS_DRAGGING_BLOCK_TARGET_POSITION.set(editor, position) - } else { - position = 'bottom' - IS_DRAGGING_BLOCK_TARGET_POSITION.set(editor, position) - } - if (isMyDragOver === element) { - event.dataTransfer.dropEffect = 'none' - return - } - setIsDragOver(true) - }, - [blockElement, editor, element], - ) - - // Note: this is called not for the dragging block, but for the targets when the block is dragged over them - const handleDragLeave = useCallback(() => { - setIsDragOver(false) - }, []) - - // Note: this is called for the dragging block - const handleDragEnd = useCallback( - (event: DragEvent) => { - const targetBlock = IS_DRAGGING_ELEMENT_TARGET.get(editor) - if (targetBlock) { - IS_DRAGGING.set(editor, false) - event.preventDefault() - event.stopPropagation() - IS_DRAGGING_ELEMENT_TARGET.delete(editor) - if (dragGhostRef.current) { - debug('Removing drag ghost') - document.body.removeChild(dragGhostRef.current) - } - const dragPosition = IS_DRAGGING_BLOCK_TARGET_POSITION.get(editor) - IS_DRAGGING_BLOCK_TARGET_POSITION.delete(editor) - let targetPath = ReactEditor.findPath(editor, targetBlock) - const myPath = ReactEditor.findPath(editor, element) - const isBefore = Path.isBefore(myPath, targetPath) - if (dragPosition === 'bottom' && !isBefore) { - // If it is already at the bottom, don't do anything. - if (targetPath[0] >= editor.children.length - 1) { - debug('target is already at the bottom, not moving') - return - } - const originalPath = targetPath - targetPath = Path.next(targetPath) - debug( - `Adjusting targetPath from ${JSON.stringify(originalPath)} to ${JSON.stringify( - targetPath, - )}`, - ) - } - if (dragPosition === 'top' && isBefore && targetPath[0] !== editor.children.length - 1) { - const originalPath = targetPath - targetPath = Path.previous(targetPath) - debug( - `Adjusting targetPath from ${JSON.stringify(originalPath)} to ${JSON.stringify( - targetPath, - )}`, - ) - } - if (Path.equals(targetPath, myPath)) { - event.preventDefault() - debug('targetPath and myPath is the same, not moving') - return - } - debug( - `Moving element ${element._key} from path ${JSON.stringify(myPath)} to ${JSON.stringify( - targetPath, - )} (${dragPosition})`, - ) - Transforms.moveNodes(editor, {at: myPath, to: targetPath}) - editor.onChange() - return - } - debug('No target element, not doing anything') - }, - [editor, element], - ) - // Note: this is called not for the dragging block, but for the drop target - const handleDrop = useCallback( - (event: DragEvent) => { - if (IS_DRAGGING_BLOCK_ELEMENT.get(editor)) { - debug('On drop (prevented)', element) - event.preventDefault() - event.stopPropagation() - setIsDragOver(false) - } - }, - [editor, element], - ) - // Note: this is called for the dragging block - const handleDrag = useCallback( - (event: DragEvent) => { - if (!isVoid) { - IS_DRAGGING_BLOCK_ELEMENT.delete(editor) - return - } - IS_DRAGGING.set(editor, true) - IS_DRAGGING_BLOCK_ELEMENT.set(editor, element) - event.stopPropagation() // Stop propagation so that leafs don't get this and take focus/selection! - - const target = event.target - - if (target instanceof HTMLElement) { - target.style.opacity = '1' - } - }, - [editor, element, isVoid], - ) - - // Note: this is called for the dragging block - const handleDragStart = useCallback( - (event: DragEvent) => { - if (!isVoid || isInline) { - debug('Not dragging block') - IS_DRAGGING_BLOCK_ELEMENT.delete(editor) - IS_DRAGGING.set(editor, false) - return - } - debug('Drag start') - IS_DRAGGING.set(editor, true) - if (event.dataTransfer) { - event.dataTransfer.setData('application/portable-text', 'something') - event.dataTransfer.effectAllowed = 'move' - } - // Clone blockElement so that it will not be visually clipped by scroll-containers etc. - // The application that uses the portable-text-editor may indicate the element used as - // drag ghost by adding a truthy data attribute 'data-pt-drag-ghost-element' to a HTML element. - if (blockElement && blockElement instanceof HTMLElement) { - let dragGhost = blockElement.cloneNode(true) as HTMLElement - const customGhost = dragGhost.querySelector('[data-pt-drag-ghost-element]') - if (customGhost) { - dragGhost = customGhost as HTMLElement - } - - // Set the `data-dragged` attribute so the consumer can style the element while it’s dragged - dragGhost.setAttribute('data-dragged', '') - - if (document.body) { - dragGhostRef.current = dragGhost - dragGhost.style.position = 'absolute' - dragGhost.style.left = '-99999px' - dragGhost.style.boxSizing = 'border-box' - document.body.appendChild(dragGhost) - const rect = blockElement.getBoundingClientRect() - const x = event.clientX - rect.left - const y = event.clientY - rect.top - dragGhost.style.width = `${rect.width}px` - dragGhost.style.height = `${rect.height}px` - event.dataTransfer.setDragImage(dragGhost, x, y) - } - } - handleDrag(event) - }, - [blockElement, editor, handleDrag, isInline, isVoid], - ) - - const isDraggingOverFirstBlock = - isDragOver && editor.children[0] === IS_DRAGGING_ELEMENT_TARGET.get(editor) - const isDraggingOverLastBlock = - isDragOver && - editor.children[editor.children.length - 1] === IS_DRAGGING_ELEMENT_TARGET.get(editor) - const dragPosition = IS_DRAGGING_BLOCK_TARGET_POSITION.get(editor) - - const isDraggingOverTop = - isDraggingOverFirstBlock || - (isDragOver && !isDraggingOverFirstBlock && !isDraggingOverLastBlock && dragPosition === 'top') - const isDraggingOverBottom = - isDraggingOverLastBlock || - (isDragOver && - !isDraggingOverFirstBlock && - !isDraggingOverLastBlock && - dragPosition === 'bottom') - - const dropIndicator = useMemo( - () => ( -
    - ), - [], - ) - - if (readOnly) { - return <>{children} - } - - if (debugRenders) { - debug('render') - } - - return ( -
    - {isDraggingOverTop && dropIndicator} - {children} - {isDraggingOverBottom && dropIndicator} -
    - ) -} diff --git a/packages/@sanity/portable-text-editor/src/editor/components/Element.tsx b/packages/@sanity/portable-text-editor/src/editor/components/Element.tsx deleted file mode 100644 index 8cc506020f3..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/components/Element.tsx +++ /dev/null @@ -1,279 +0,0 @@ -/* eslint-disable complexity */ -/* eslint-disable max-statements */ -import { - type Path, - type PortableTextChild, - type PortableTextObject, - type PortableTextTextBlock, -} from '@sanity/types' -import {type FunctionComponent, type ReactElement, useMemo, useRef} from 'react' -import {Editor, Element as SlateElement, Range} from 'slate' -import {ReactEditor, type RenderElementProps, useSelected, useSlateStatic} from 'slate-react' - -import { - type BlockRenderProps, - type PortableTextMemberSchemaTypes, - type RenderBlockFunction, - type RenderChildFunction, - type RenderListItemFunction, - type RenderStyleFunction, -} from '../../types/editor' -import {debugWithName} from '../../utils/debug' -import {fromSlateValue} from '../../utils/values' -import {KEY_TO_VALUE_ELEMENT} from '../../utils/weakMaps' -import ObjectNode from '../nodes/DefaultObject' -import {DefaultBlockObject, DefaultListItem, DefaultListItemInner} from '../nodes/index' -import {DraggableBlock} from './DraggableBlock' - -const debug = debugWithName('components:Element') -const debugRenders = false -const EMPTY_ANNOTATIONS: PortableTextObject[] = [] - -/** - * @internal - */ -export interface ElementProps { - attributes: RenderElementProps['attributes'] - children: ReactElement - element: SlateElement - schemaTypes: PortableTextMemberSchemaTypes - readOnly: boolean - renderBlock?: RenderBlockFunction - renderChild?: RenderChildFunction - renderListItem?: RenderListItemFunction - renderStyle?: RenderStyleFunction - spellCheck?: boolean -} - -const inlineBlockStyle = {display: 'inline-block'} - -/** - * Renders Portable Text block and inline object nodes in Slate - * @internal - */ -export const Element: FunctionComponent = ({ - attributes, - children, - element, - schemaTypes, - readOnly, - renderBlock, - renderChild, - renderListItem, - renderStyle, - spellCheck, -}) => { - const editor = useSlateStatic() - const selected = useSelected() - const blockRef = useRef(null) - const inlineBlockObjectRef = useRef(null) - const focused = (selected && editor.selection && Range.isCollapsed(editor.selection)) || false - - const value = useMemo( - () => fromSlateValue([element], schemaTypes.block.name, KEY_TO_VALUE_ELEMENT.get(editor))[0], - [editor, element, schemaTypes.block.name], - ) - - let renderedBlock = children - - let className - - const blockPath: Path = useMemo(() => [{_key: element._key}], [element]) - - if (typeof element._type !== 'string') { - throw new Error(`Expected element to have a _type property`) - } - - if (typeof element._key !== 'string') { - throw new Error(`Expected element to have a _key property`) - } - - // Test for inline objects first - if (editor.isInline(element)) { - const path = ReactEditor.findPath(editor, element) - const [block] = Editor.node(editor, path, {depth: 1}) - const schemaType = schemaTypes.inlineObjects.find((_type) => _type.name === element._type) - if (!schemaType) { - throw new Error('Could not find type for inline block element') - } - if (SlateElement.isElement(block)) { - const elmPath: Path = [{_key: block._key}, 'children', {_key: element._key}] - if (debugRenders) { - debug(`Render ${element._key} (inline object)`) - } - return ( - - {/* Note that children must follow immediately or cut and selections will not work properly in Chrome. */} - {children} - - {renderChild && - renderChild({ - annotations: EMPTY_ANNOTATIONS, // These inline objects currently doesn't support annotations. This is a limitation of the current PT spec/model. - children: , - editorElementRef: inlineBlockObjectRef, - focused, - path: elmPath, - schemaType, - selected, - type: schemaType, - value: value as PortableTextChild, - })} - {!renderChild && } - - - ) - } - throw new Error('Block not found!') - } - - // If not inline, it's either a block (text) or a block object (non-text) - // NOTE: text blocks aren't draggable with DraggableBlock (yet?) - if (element._type === schemaTypes.block.name) { - className = `pt-block pt-text-block` - const isListItem = 'listItem' in element - if (debugRenders) { - debug(`Render ${element._key} (text block)`) - } - const style = ('style' in element && element.style) || 'normal' - className = `pt-block pt-text-block pt-text-block-style-${style}` - const blockStyleType = schemaTypes.styles.find((item) => item.value === style) - if (renderStyle && blockStyleType) { - renderedBlock = renderStyle({ - block: element as PortableTextTextBlock, - children, - focused, - selected, - value: style, - path: blockPath, - schemaType: blockStyleType, - editorElementRef: blockRef, - }) - } - let level - if (isListItem) { - if (typeof element.level === 'number') { - level = element.level - } - className += ` pt-list-item pt-list-item-${element.listItem} pt-list-item-level-${level || 1}` - } - if (editor.isListBlock(value) && isListItem && element.listItem) { - const listType = schemaTypes.lists.find((item) => item.value === element.listItem) - if (renderListItem && listType) { - renderedBlock = renderListItem({ - block: value, - children: renderedBlock, - focused, - selected, - value: element.listItem, - path: blockPath, - schemaType: listType, - level: value.level || 1, - editorElementRef: blockRef, - }) - } else { - renderedBlock = ( - - {renderedBlock} - - ) - } - } - const renderProps: Omit = Object.defineProperty( - { - children: renderedBlock, - editorElementRef: blockRef, - focused, - level, - listItem: isListItem ? element.listItem : undefined, - path: blockPath, - selected, - style, - schemaType: schemaTypes.block, - value, - }, - 'type', - { - enumerable: false, - get() { - console.warn("Property 'type' is deprecated, use 'schemaType' instead.") - return schemaTypes.block - }, - }, - ) - - const propsOrDefaultRendered = renderBlock - ? renderBlock(renderProps as BlockRenderProps) - : children - return ( -
    - -
    {propsOrDefaultRendered}
    -
    -
    - ) - } - const schemaType = schemaTypes.blockObjects.find((_type) => _type.name === element._type) - if (!schemaType) { - throw new Error(`Could not find schema type for block element of _type ${element._type}`) - } - if (debugRenders) { - debug(`Render ${element._key} (object block)`) - } - className = 'pt-block pt-object-block' - const block = fromSlateValue( - [element], - schemaTypes.block.name, - KEY_TO_VALUE_ELEMENT.get(editor), - )[0] - let renderedBlockFromProps - if (renderBlock) { - const _props: Omit = Object.defineProperty( - { - children: , - editorElementRef: blockRef, - focused, - path: blockPath, - schemaType, - selected, - value: block, - }, - 'type', - { - enumerable: false, - get() { - console.warn("Property 'type' is deprecated, use 'schemaType' instead.") - return schemaType - }, - }, - ) - renderedBlockFromProps = renderBlock(_props as BlockRenderProps) - } - return ( -
    - {children} - - {renderedBlockFromProps && ( -
    - {renderedBlockFromProps} -
    - )} - {!renderedBlockFromProps && ( - - - - )} -
    -
    - ) -} diff --git a/packages/@sanity/portable-text-editor/src/editor/components/Leaf.tsx b/packages/@sanity/portable-text-editor/src/editor/components/Leaf.tsx deleted file mode 100644 index 7eb2c83afd9..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/components/Leaf.tsx +++ /dev/null @@ -1,288 +0,0 @@ -import {type Path, type PortableTextObject, type PortableTextTextBlock} from '@sanity/types' -import {isEqual, uniq} from 'lodash' -import { - type ReactElement, - startTransition, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react' -import {Text} from 'slate' -import {type RenderLeafProps, useSelected} from 'slate-react' - -import { - type BlockAnnotationRenderProps, - type BlockChildRenderProps, - type BlockDecoratorRenderProps, - type PortableTextMemberSchemaTypes, - type RenderAnnotationFunction, - type RenderChildFunction, - type RenderDecoratorFunction, -} from '../../types/editor' -import {debugWithName} from '../../utils/debug' -import {usePortableTextEditor} from '../hooks/usePortableTextEditor' -import {DefaultAnnotation} from '../nodes/DefaultAnnotation' -import {PortableTextEditor} from '../PortableTextEditor' - -const debug = debugWithName('components:Leaf') - -const EMPTY_MARKS: string[] = [] - -/** - * @internal - */ -export interface LeafProps extends RenderLeafProps { - children: ReactElement - schemaTypes: PortableTextMemberSchemaTypes - renderAnnotation?: RenderAnnotationFunction - renderChild?: RenderChildFunction - renderDecorator?: RenderDecoratorFunction - readOnly: boolean -} - -/** - * Renders Portable Text span nodes in Slate - * @internal - */ -export const Leaf = (props: LeafProps) => { - const {attributes, children, leaf, schemaTypes, renderChild, renderDecorator, renderAnnotation} = - props - const spanRef = useRef(null) - const portableTextEditor = usePortableTextEditor() - const blockSelected = useSelected() - const [focused, setFocused] = useState(false) - const [selected, setSelected] = useState(false) - const block = children.props.parent as PortableTextTextBlock | undefined - const path: Path = useMemo( - () => (block ? [{_key: block?._key}, 'children', {_key: leaf._key}] : []), - [block, leaf._key], - ) - const decoratorValues = useMemo( - () => schemaTypes.decorators.map((dec) => dec.value), - [schemaTypes.decorators], - ) - const marks: string[] = useMemo( - () => uniq((leaf.marks || EMPTY_MARKS).filter((mark) => decoratorValues.includes(mark))), - [decoratorValues, leaf.marks], - ) - const annotationMarks = Array.isArray(leaf.marks) ? leaf.marks : EMPTY_MARKS - const annotations = useMemo( - () => - annotationMarks - .map( - (mark) => - !decoratorValues.includes(mark) && block?.markDefs?.find((def) => def._key === mark), - ) - .filter(Boolean) as PortableTextObject[], - [annotationMarks, block, decoratorValues], - ) - - const shouldTrackSelectionAndFocus = annotations.length > 0 && blockSelected - - useEffect(() => { - if (!shouldTrackSelectionAndFocus) { - setFocused(false) - return - } - const sel = PortableTextEditor.getSelection(portableTextEditor) - if ( - sel && - isEqual(sel.focus.path, path) && - PortableTextEditor.isCollapsedSelection(portableTextEditor) - ) { - startTransition(() => { - setFocused(true) - }) - } - }, [shouldTrackSelectionAndFocus, path, portableTextEditor]) - - // Function to check if this leaf is currently inside the user's text selection - const setSelectedFromRange = useCallback(() => { - if (!shouldTrackSelectionAndFocus) { - return - } - debug('Setting selection and focus from range') - const winSelection = window.getSelection() - if (!winSelection) { - setSelected(false) - return - } - if (winSelection && winSelection.rangeCount > 0) { - const range = winSelection.getRangeAt(0) - if (spanRef.current && range.intersectsNode(spanRef.current)) { - setSelected(true) - } else { - setSelected(false) - } - } else { - setSelected(false) - } - }, [shouldTrackSelectionAndFocus]) - - useEffect(() => { - if (!shouldTrackSelectionAndFocus) { - return undefined - } - const sub = portableTextEditor.change$.subscribe((next) => { - if (next.type === 'blur') { - setFocused(false) - setSelected(false) - return - } - if (next.type === 'focus') { - const sel = PortableTextEditor.getSelection(portableTextEditor) - if ( - sel && - isEqual(sel.focus.path, path) && - PortableTextEditor.isCollapsedSelection(portableTextEditor) - ) { - setFocused(true) - } - setSelectedFromRange() - return - } - if (next.type === 'selection') { - if ( - next.selection && - isEqual(next.selection.focus.path, path) && - PortableTextEditor.isCollapsedSelection(portableTextEditor) - ) { - setFocused(true) - } else { - setFocused(false) - } - setSelectedFromRange() - } - }) - return () => { - sub.unsubscribe() - } - }, [path, portableTextEditor, setSelectedFromRange, shouldTrackSelectionAndFocus]) - - useEffect(() => setSelectedFromRange(), [setSelectedFromRange]) - - const content = useMemo(() => { - let returnedChildren = children - // Render text nodes - if (Text.isText(leaf) && leaf._type === schemaTypes.span.name) { - marks.forEach((mark) => { - const schemaType = schemaTypes.decorators.find((dec) => dec.value === mark) - if (schemaType && renderDecorator) { - const _props: Omit = Object.defineProperty( - { - children: returnedChildren, - editorElementRef: spanRef, - focused, - path, - selected, - schemaType, - value: mark, - }, - 'type', - { - enumerable: false, - get() { - console.warn("Property 'type' is deprecated, use 'schemaType' instead.") - return schemaType - }, - }, - ) - returnedChildren = renderDecorator(_props as BlockDecoratorRenderProps) - } - }) - - if (block && annotations.length > 0) { - annotations.forEach((annotation) => { - const schemaType = schemaTypes.annotations.find((t) => t.name === annotation._type) - if (schemaType) { - if (renderAnnotation) { - const _props: Omit = Object.defineProperty( - { - block, - children: returnedChildren, - editorElementRef: spanRef, - focused, - path, - selected, - schemaType, - value: annotation, - }, - 'type', - { - enumerable: false, - get() { - console.warn("Property 'type' is deprecated, use 'schemaType' instead.") - return schemaType - }, - }, - ) - - returnedChildren = ( - {renderAnnotation(_props as BlockAnnotationRenderProps)} - ) - } else { - returnedChildren = ( - - {returnedChildren} - - ) - } - } - }) - } - if (block && renderChild) { - const child = block.children.find((_child) => _child._key === leaf._key) // Ensure object equality - if (child) { - const defaultRendered = <>{returnedChildren} - const _props: Omit = Object.defineProperty( - { - annotations, - children: defaultRendered, - editorElementRef: spanRef, - focused, - path, - schemaType: schemaTypes.span, - selected, - value: child, - }, - 'type', - { - enumerable: false, - get() { - console.warn("Property 'type' is deprecated, use 'schemaType' instead.") - return schemaTypes.span - }, - }, - ) - returnedChildren = renderChild(_props as BlockChildRenderProps) - } - } - } - return returnedChildren - }, [ - annotations, - block, - children, - focused, - leaf, - marks, - path, - renderAnnotation, - renderChild, - renderDecorator, - schemaTypes.annotations, - schemaTypes.decorators, - schemaTypes.span, - selected, - ]) - return useMemo( - () => ( - - {content} - - ), - [leaf, attributes, content], - ) -} diff --git a/packages/@sanity/portable-text-editor/src/editor/components/SlateContainer.tsx b/packages/@sanity/portable-text-editor/src/editor/components/SlateContainer.tsx deleted file mode 100644 index 4189b8fe172..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/components/SlateContainer.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import {type PropsWithChildren, useEffect, useMemo, useState} from 'react' -import {createEditor} from 'slate' -import {Slate, withReact} from 'slate-react' - -import {type PatchObservable} from '../../types/editor' -import {debugWithName} from '../../utils/debug' -import {KEY_TO_SLATE_ELEMENT, KEY_TO_VALUE_ELEMENT} from '../../utils/weakMaps' -import {withPlugins} from '../plugins' -import {type PortableTextEditor} from '../PortableTextEditor' - -const debug = debugWithName('component:PortableTextEditor:SlateContainer') - -/** - * @internal - */ -export interface SlateContainerProps extends PropsWithChildren { - keyGenerator: () => string - maxBlocks: number | undefined - patches$?: PatchObservable - portableTextEditor: PortableTextEditor - readOnly: boolean -} - -/** - * Sets up and encapsulates the Slate instance - * @internal - */ -export function SlateContainer(props: SlateContainerProps) { - const {patches$, portableTextEditor, readOnly, maxBlocks, keyGenerator} = props - - // Create the slate instance, using `useState` ensures setup is only run once, initially - const [[slateEditor, subscribe]] = useState(() => { - debug('Creating new Slate editor instance') - const {editor, subscribe: _sub} = withPlugins(withReact(createEditor()), { - keyGenerator, - maxBlocks, - patches$, - portableTextEditor, - readOnly, - }) - KEY_TO_VALUE_ELEMENT.set(editor, {}) - KEY_TO_SLATE_ELEMENT.set(editor, {}) - return [editor, _sub] as const - }) - - useEffect(() => { - const unsubscribe = subscribe() - return () => { - unsubscribe() - } - }, [subscribe]) - - // Update the slate instance when plugin dependent props change. - useEffect(() => { - debug('Re-initializing plugin chain') - withPlugins(slateEditor, { - keyGenerator, - maxBlocks, - patches$, - portableTextEditor, - readOnly, - }) - }, [keyGenerator, portableTextEditor, maxBlocks, readOnly, patches$, slateEditor]) - - const initialValue = useMemo(() => { - return [slateEditor.pteCreateEmptyBlock()] - }, [slateEditor]) - - useEffect(() => { - return () => { - debug('Destroying Slate editor') - slateEditor.destroy() - } - }, [slateEditor]) - - return ( - - {props.children} - - ) -} diff --git a/packages/@sanity/portable-text-editor/src/editor/components/Synchronizer.tsx b/packages/@sanity/portable-text-editor/src/editor/components/Synchronizer.tsx deleted file mode 100644 index 71ddef9a583..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/components/Synchronizer.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import {type PortableTextBlock} from '@sanity/types' -import {throttle} from 'lodash' -import { - type PropsWithChildren, - startTransition, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react' -import {Editor} from 'slate' -import {useSlate} from 'slate-react' - -import {type EditorChange, type EditorChanges, type EditorSelection} from '../../types/editor' -import {type Patch} from '../../types/patch' -import {debugWithName} from '../../utils/debug' -import {IS_PROCESSING_LOCAL_CHANGES} from '../../utils/weakMaps' -import {PortableTextEditorContext} from '../hooks/usePortableTextEditor' -import {PortableTextEditorKeyGeneratorContext} from '../hooks/usePortableTextEditorKeyGenerator' -import {PortableTextEditorSelectionContext} from '../hooks/usePortableTextEditorSelection' -import {PortableTextEditorValueContext} from '../hooks/usePortableTextEditorValue' -import {PortableTextEditorReadOnlyContext} from '../hooks/usePortableTextReadOnly' -import {useSyncValue} from '../hooks/useSyncValue' -import {PortableTextEditor} from '../PortableTextEditor' - -const debug = debugWithName('component:PortableTextEditor:Synchronizer') -const debugVerbose = debug.enabled && false - -// The editor will commit changes in a throttled fashion in order -// not to overload the network and degrade performance while typing. -const FLUSH_PATCHES_THROTTLED_MS = process.env.NODE_ENV === 'test' ? 500 : 1000 - -/** - * @internal - */ -export interface SynchronizerProps extends PropsWithChildren { - change$: EditorChanges - portableTextEditor: PortableTextEditor - keyGenerator: () => string - onChange: (change: EditorChange) => void - readOnly: boolean - value: PortableTextBlock[] | undefined -} - -/** - * Synchronizes the server value with the editor, and provides various contexts for the editor state. - * @internal - */ -export function Synchronizer(props: SynchronizerProps) { - const {change$, portableTextEditor, onChange, keyGenerator, readOnly, value} = props - const [selection, setSelection] = useState(null) - const pendingPatches = useRef([]) - - const syncValue = useSyncValue({ - keyGenerator, - onChange, - portableTextEditor, - readOnly, - }) - - const slateEditor = useSlate() - - useEffect(() => { - IS_PROCESSING_LOCAL_CHANGES.set(slateEditor, false) - }, [slateEditor]) - - const onFlushPendingPatches = useCallback(() => { - if (pendingPatches.current.length > 0) { - debug('Flushing pending patches') - if (debugVerbose) { - debug(`Patches:\n${JSON.stringify(pendingPatches.current, null, 2)}`) - } - const snapshot = PortableTextEditor.getValue(portableTextEditor) - change$.next({type: 'mutation', patches: pendingPatches.current, snapshot}) - pendingPatches.current = [] - } - IS_PROCESSING_LOCAL_CHANGES.set(slateEditor, false) - }, [slateEditor, portableTextEditor, change$]) - - const onFlushPendingPatchesThrottled = useMemo(() => { - return throttle( - () => { - // If the editor is normalizing (each operation) it means that it's not in the middle of a bigger transform, - // and we can flush these changes immediately. - if (Editor.isNormalizing(slateEditor)) { - onFlushPendingPatches() - return - } - // If it's in the middle of something, try again. - onFlushPendingPatchesThrottled() - }, - FLUSH_PATCHES_THROTTLED_MS, - { - leading: false, - trailing: true, - }, - ) - }, [onFlushPendingPatches, slateEditor]) - - // Flush pending patches immediately on unmount - useEffect(() => { - return () => { - onFlushPendingPatches() - } - }, [onFlushPendingPatches]) - - // Subscribe to, and handle changes from the editor - useEffect(() => { - debug('Subscribing to editor changes$') - const sub = change$.subscribe((next: EditorChange): void => { - switch (next.type) { - case 'patch': - IS_PROCESSING_LOCAL_CHANGES.set(slateEditor, true) - pendingPatches.current.push(next.patch) - onFlushPendingPatchesThrottled() - onChange(next) - break - case 'selection': - // Set the selection state in a transition, we don't need the state immediately. - startTransition(() => { - if (debugVerbose) debug('Setting selection') - setSelection(next.selection) - }) - onChange(next) // Keep this out of the startTransition! - break - default: - onChange(next) - } - }) - return () => { - debug('Unsubscribing to changes$') - sub.unsubscribe() - } - }, [change$, onChange, onFlushPendingPatchesThrottled, slateEditor]) - - // Sync the value when going online - const handleOnline = useCallback(() => { - debug('Editor is online, syncing from props.value') - change$.next({type: 'connection', value: 'online'}) - syncValue(value) - }, [change$, syncValue, value]) - - const handleOffline = useCallback(() => { - debug('Editor is offline') - change$.next({type: 'connection', value: 'offline'}) - }, [change$]) - - // Notify about window online and offline status changes - useEffect(() => { - if (portableTextEditor.props.patches$) { - window.addEventListener('online', handleOnline) - window.addEventListener('offline', handleOffline) - } - return () => { - if (portableTextEditor.props.patches$) { - window.removeEventListener('online', handleOnline) - window.removeEventListener('offline', handleOffline) - } - } - }) - - // This hook must be set up after setting up the subscription above, or it will not pick up validation errors from the useSyncValue hook. - // This will cause the editor to not be able to signal a validation error and offer invalid value resolution of the initial value. - const isInitialValueFromProps = useRef(true) - useEffect(() => { - debug('Value from props changed, syncing new value') - syncValue(value) - // Signal that we have our first value, and are ready to roll. - if (isInitialValueFromProps.current) { - change$.next({type: 'loading', isLoading: false}) - change$.next({type: 'ready'}) - isInitialValueFromProps.current = false - } - }, [change$, syncValue, value]) - - return ( - - - - - - {props.children} - - - - - - ) -} diff --git a/packages/@sanity/portable-text-editor/src/editor/hooks/usePortableTextEditor.ts b/packages/@sanity/portable-text-editor/src/editor/hooks/usePortableTextEditor.ts deleted file mode 100644 index e95a26e269e..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/hooks/usePortableTextEditor.ts +++ /dev/null @@ -1,23 +0,0 @@ -import {createContext, useContext} from 'react' - -import {type PortableTextEditor} from '../PortableTextEditor' - -/** - * A React context for sharing the editor object. - */ -export const PortableTextEditorContext = createContext(null) - -/** - * Get the current editor object from the React context. - */ -export const usePortableTextEditor = (): PortableTextEditor => { - const editor = useContext(PortableTextEditorContext) - - if (!editor) { - throw new Error( - `The \`usePortableTextEditor\` hook must be used inside the component's context.`, - ) - } - - return editor -} diff --git a/packages/@sanity/portable-text-editor/src/editor/hooks/usePortableTextEditorKeyGenerator.ts b/packages/@sanity/portable-text-editor/src/editor/hooks/usePortableTextEditorKeyGenerator.ts deleted file mode 100644 index 7fd279539d2..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/hooks/usePortableTextEditorKeyGenerator.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {randomKey} from '@sanity/util/content' -import {createContext, useContext} from 'react' - -export const defaultKeyGenerator = (): string => randomKey(12) - -/** - * A React context for sharing the editor's keyGenerator. - */ -export const PortableTextEditorKeyGeneratorContext = - createContext<() => string>(defaultKeyGenerator) - -/** - * Get the current editor selection from the React context. - */ -export const usePortableTextEditorKeyGenerator = (): (() => string) => { - const keyGenerator = useContext(PortableTextEditorKeyGeneratorContext) - - if (keyGenerator === undefined) { - throw new Error( - `The \`usePortableTextEditorKeyGenerator\` hook must be used inside the component's context.`, - ) - } - return keyGenerator -} diff --git a/packages/@sanity/portable-text-editor/src/editor/hooks/usePortableTextEditorSelection.ts b/packages/@sanity/portable-text-editor/src/editor/hooks/usePortableTextEditorSelection.ts deleted file mode 100644 index 91cfec99774..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/hooks/usePortableTextEditorSelection.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {createContext, useContext} from 'react' - -import {type EditorSelection} from '../../types/editor' - -/** - * A React context for sharing the editor selection. - */ -export const PortableTextEditorSelectionContext = createContext(null) - -/** - * Get the current editor selection from the React context. - */ -export const usePortableTextEditorSelection = (): EditorSelection => { - const selection = useContext(PortableTextEditorSelectionContext) - - if (selection === undefined) { - throw new Error( - `The \`usePortableTextEditorSelection\` hook must be used inside the component's context.`, - ) - } - return selection -} diff --git a/packages/@sanity/portable-text-editor/src/editor/hooks/usePortableTextEditorValue.ts b/packages/@sanity/portable-text-editor/src/editor/hooks/usePortableTextEditorValue.ts deleted file mode 100644 index f8a9cd3d0f0..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/hooks/usePortableTextEditorValue.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {type PortableTextBlock} from '@sanity/types' -import {createContext, useContext} from 'react' - -/** - * A React context for sharing the editor value. - */ -export const PortableTextEditorValueContext = createContext( - undefined, -) - -/** - * Get the current editor value from the React context. - */ -export const usePortableTextEditorValue = () => { - return useContext(PortableTextEditorValueContext) -} diff --git a/packages/@sanity/portable-text-editor/src/editor/hooks/usePortableTextReadOnly.ts b/packages/@sanity/portable-text-editor/src/editor/hooks/usePortableTextReadOnly.ts deleted file mode 100644 index cd26356ffbd..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/hooks/usePortableTextReadOnly.ts +++ /dev/null @@ -1,20 +0,0 @@ -import {createContext, useContext} from 'react' - -/** - * A React context for sharing the editor's readOnly status. - */ -export const PortableTextEditorReadOnlyContext = createContext(false) - -/** - * Get the current editor selection from the React context. - */ -export const usePortableTextEditorReadOnlyStatus = (): boolean => { - const readOnly = useContext(PortableTextEditorReadOnlyContext) - - if (readOnly === undefined) { - throw new Error( - `The \`usePortableTextEditorReadOnly\` hook must be used inside the component's context.`, - ) - } - return readOnly -} diff --git a/packages/@sanity/portable-text-editor/src/editor/hooks/useSyncValue.test.tsx b/packages/@sanity/portable-text-editor/src/editor/hooks/useSyncValue.test.tsx deleted file mode 100644 index e24d7f8d356..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/hooks/useSyncValue.test.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import {describe, expect, it, jest} from '@jest/globals' -import {render, waitFor} from '@testing-library/react' -import {createRef, type RefObject} from 'react' - -import {PortableTextEditorTester, schemaType} from '../__tests__/PortableTextEditorTester' -import {PortableTextEditor} from '../PortableTextEditor' - -const initialValue = [ - { - _key: '77071c3af231', - _type: 'myTestBlockType', - children: [ - { - _key: 'c001f0e92c1f0', - _type: 'span', - marks: [], - text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ', - }, - ], - markDefs: [], - style: 'normal', - }, -] - -describe('useSyncValue', () => { - it('updates span text', async () => { - const editorRef: RefObject = createRef() - const onChange = jest.fn() - const syncedValue = [ - { - _key: '77071c3af231', - _type: 'myTestBlockType', - children: [ - { - _key: 'c001f0e92c1f0', - _type: 'span', - marks: [], - text: 'Lorem my ipsum!', - }, - ], - markDefs: [], - style: 'normal', - }, - ] - const {rerender} = render( - , - ) - rerender( - , - ) - await waitFor(() => { - if (editorRef.current) { - expect(PortableTextEditor.getValue(editorRef.current)).toEqual(syncedValue) - } - }) - }) - it('replaces span nodes with different keys inside the same children array', async () => { - const editorRef: RefObject = createRef() - const onChange = jest.fn() - const syncedValue = [ - { - _key: '77071c3af231', - _type: 'myTestBlockType', - children: [ - { - _key: 'c001f0e92c1f0__NEW_KEY_YA!', - _type: 'span', - marks: [], - text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ', - }, - ], - markDefs: [], - style: 'normal', - }, - ] - const {rerender} = render( - , - ) - rerender( - , - ) - await waitFor(() => { - if (editorRef.current) { - expect(PortableTextEditor.getValue(editorRef.current)).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "77071c3af231", - "_type": "myTestBlockType", - "children": Array [ - Object { - "_key": "c001f0e92c1f0__NEW_KEY_YA!", - "_type": "span", - "marks": Array [], - "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. ", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - } - }) - }) -}) diff --git a/packages/@sanity/portable-text-editor/src/editor/hooks/useSyncValue.ts b/packages/@sanity/portable-text-editor/src/editor/hooks/useSyncValue.ts deleted file mode 100644 index 186adf9829b..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/hooks/useSyncValue.ts +++ /dev/null @@ -1,372 +0,0 @@ -/* eslint-disable max-nested-callbacks */ -import {type PortableTextBlock} from '@sanity/types' -import {debounce, isEqual} from 'lodash' -import {useCallback, useMemo, useRef} from 'react' -import {type Descendant, Editor, type Node, Text, Transforms} from 'slate' -import {useSlate} from 'slate-react' - -import {type EditorChange, type PortableTextSlateEditor} from '../../types/editor' -import {debugWithName} from '../../utils/debug' -import {validateValue} from '../../utils/validateValue' -import {toSlateValue, VOID_CHILD_KEY} from '../../utils/values' -import {isChangingLocally, isChangingRemotely, withRemoteChanges} from '../../utils/withChanges' -import {withoutPatching} from '../../utils/withoutPatching' -import {withPreserveKeys} from '../../utils/withPreserveKeys' -import {withoutSaving} from '../plugins/createWithUndoRedo' -import {type PortableTextEditor} from '../PortableTextEditor' - -const debug = debugWithName('hook:useSyncValue') - -/** - * @internal - */ -export interface UseSyncValueProps { - keyGenerator: () => string - onChange: (change: EditorChange) => void - portableTextEditor: PortableTextEditor - readOnly: boolean -} - -const CURRENT_VALUE = new WeakMap() - -/** - * Sync value with the editor state - * - * Normally nothing here should apply, and the editor and the real world are perfectly aligned. - * - * Inconsistencies could happen though, so we need to check the editor state when the value changes. - * - * For performance reasons, it makes sense to also do the content validation here, as we already - * iterate over the value and can validate only the new content that is actually changed. - * - * @internal - */ -export function useSyncValue( - props: UseSyncValueProps, -): (value: PortableTextBlock[] | undefined, userCallbackFn?: () => void) => void { - const {portableTextEditor, readOnly, keyGenerator} = props - const {change$, schemaTypes} = portableTextEditor - const previousValue = useRef() - const slateEditor = useSlate() - const updateValueFunctionRef = useRef<(value: PortableTextBlock[] | undefined) => void>() - - const updateFromCurrentValue = useCallback(() => { - const currentValue = CURRENT_VALUE.get(portableTextEditor) - if (previousValue.current === currentValue) { - debug('Value is the same object as previous, not need to sync') - return - } - if (updateValueFunctionRef.current && currentValue) { - debug('Updating the value debounced') - updateValueFunctionRef.current(currentValue) - } - }, [portableTextEditor]) - const updateValueDebounced = useMemo( - () => debounce(updateFromCurrentValue, 1000, {trailing: true, leading: false}), - [updateFromCurrentValue], - ) - - return useMemo(() => { - const updateFunction = (value: PortableTextBlock[] | undefined) => { - CURRENT_VALUE.set(portableTextEditor, value) - const isProcessingLocalChanges = isChangingLocally(slateEditor) - const isProcessingRemoteChanges = isChangingRemotely(slateEditor) - if (!readOnly) { - if (isProcessingLocalChanges) { - debug('Has local changes, not syncing value right now') - updateValueDebounced() - return - } - if (isProcessingRemoteChanges) { - debug('Has remote changes, not syncing value right now') - updateValueDebounced() - return - } - } - - let isChanged = false - let isValid = true - - const hadSelection = !!slateEditor.selection - - // If empty value, remove everything in the editor and insert a placeholder block - if (!value || value.length === 0) { - debug('Value is empty') - Editor.withoutNormalizing(slateEditor, () => { - withoutSaving(slateEditor, () => { - withoutPatching(slateEditor, () => { - if (hadSelection) { - Transforms.deselect(slateEditor) - } - const childrenLength = slateEditor.children.length - slateEditor.children.forEach((_, index) => { - Transforms.removeNodes(slateEditor, { - at: [childrenLength - 1 - index], - }) - }) - Transforms.insertNodes(slateEditor, slateEditor.pteCreateEmptyBlock(), {at: [0]}) - // Add a new selection in the top of the document - if (hadSelection) { - Transforms.select(slateEditor, [0, 0]) - } - }) - }) - }) - isChanged = true - } - // Remove, replace or add nodes according to what is changed. - if (value && value.length > 0) { - const slateValueFromProps = toSlateValue(value, { - schemaTypes, - }) - Editor.withoutNormalizing(slateEditor, () => { - withRemoteChanges(slateEditor, () => { - withoutSaving(slateEditor, () => { - withoutPatching(slateEditor, () => { - const childrenLength = slateEditor.children.length - // Remove blocks that have become superfluous - if (slateValueFromProps.length < childrenLength) { - for (let i = childrenLength - 1; i > slateValueFromProps.length - 1; i--) { - Transforms.removeNodes(slateEditor, { - at: [i], - }) - } - isChanged = true - } - // Go through all of the blocks and see if they need to be updated - slateValueFromProps.forEach((currentBlock, currentBlockIndex) => { - const oldBlock = slateEditor.children[currentBlockIndex] - const hasChanges = oldBlock && !isEqual(currentBlock, oldBlock) - if (hasChanges && isValid) { - const validationValue = [value[currentBlockIndex]] - const validation = validateValue(validationValue, schemaTypes, keyGenerator) - // Resolve validations that can be resolved automatically, without involving the user (but only if the value was changed) - if ( - !validation.valid && - validation.resolution?.autoResolve && - validation.resolution?.patches.length > 0 - ) { - // Only apply auto resolution if the value has been populated before and is different from the last one. - if (!readOnly && previousValue.current && previousValue.current !== value) { - // Give a console warning about the fact that it did an auto resolution - console.warn( - `${validation.resolution.action} for block with _key '${validationValue[0]._key}'. ${validation.resolution?.description}`, - ) - validation.resolution.patches.forEach((patch) => { - change$.next({type: 'patch', patch}) - }) - } - } - if (validation.valid || validation.resolution?.autoResolve) { - if (oldBlock._key === currentBlock._key) { - if (debug.enabled) debug('Updating block', oldBlock, currentBlock) - _updateBlock(slateEditor, currentBlock, oldBlock, currentBlockIndex) - } else { - if (debug.enabled) debug('Replacing block', oldBlock, currentBlock) - _replaceBlock(slateEditor, currentBlock, currentBlockIndex) - } - isChanged = true - } else { - change$.next({ - type: 'invalidValue', - resolution: validation.resolution, - value, - }) - isValid = false - } - } - if (!oldBlock && isValid) { - const validationValue = [value[currentBlockIndex]] - const validation = validateValue(validationValue, schemaTypes, keyGenerator) - if (debug.enabled) - debug( - 'Validating and inserting new block in the end of the value', - currentBlock, - ) - if (validation.valid || validation.resolution?.autoResolve) { - withPreserveKeys(slateEditor, () => { - Transforms.insertNodes(slateEditor, currentBlock, { - at: [currentBlockIndex], - }) - }) - } else { - debug('Invalid', validation) - change$.next({ - type: 'invalidValue', - resolution: validation.resolution, - value, - }) - isValid = false - } - } - }) - }) - }) - }) - }) - } - - if (!isValid) { - debug('Invalid value, returning') - return - } - if (isChanged) { - debug('Server value changed, syncing editor') - try { - slateEditor.onChange() - } catch (err) { - console.error(err) - change$.next({ - type: 'invalidValue', - resolution: null, - value, - }) - return - } - if (hadSelection && !slateEditor.selection) { - Transforms.select(slateEditor, { - anchor: {path: [0, 0], offset: 0}, - focus: {path: [0, 0], offset: 0}, - }) - slateEditor.onChange() - } - change$.next({type: 'value', value}) - } else { - debug('Server value and editor value is equal, no need to sync.') - } - previousValue.current = value - } - updateValueFunctionRef.current = updateFunction - return updateFunction - }, [ - change$, - keyGenerator, - portableTextEditor, - readOnly, - schemaTypes, - slateEditor, - updateValueDebounced, - ]) -} - -/** - * This code is moved out of the above algorithm to keep complexity down. - * @internal - */ -function _replaceBlock( - slateEditor: PortableTextSlateEditor, - currentBlock: Descendant, - currentBlockIndex: number, -) { - // While replacing the block and the current selection focus is on the replaced block, - // temporarily deselect the editor then optimistically try to restore the selection afterwards. - const currentSelection = slateEditor.selection - const selectionFocusOnBlock = - currentSelection && currentSelection.focus.path[0] === currentBlockIndex - if (selectionFocusOnBlock) { - Transforms.deselect(slateEditor) - } - Transforms.removeNodes(slateEditor, {at: [currentBlockIndex]}) - withPreserveKeys(slateEditor, () => { - Transforms.insertNodes(slateEditor, currentBlock, {at: [currentBlockIndex]}) - }) - slateEditor.onChange() - if (selectionFocusOnBlock) { - Transforms.select(slateEditor, currentSelection) - } -} - -/** - * This code is moved out of the above algorithm to keep complexity down. - * @internal - */ -function _updateBlock( - slateEditor: PortableTextSlateEditor, - currentBlock: Descendant, - oldBlock: Descendant, - currentBlockIndex: number, -) { - // Update the root props on the block - Transforms.setNodes(slateEditor, currentBlock as Partial, { - at: [currentBlockIndex], - }) - // Text block's need to have their children updated as well (setNode does not target a node's children) - if (slateEditor.isTextBlock(currentBlock) && slateEditor.isTextBlock(oldBlock)) { - const oldBlockChildrenLength = oldBlock.children.length - if (currentBlock.children.length < oldBlockChildrenLength) { - // Remove any children that have become superfluous - Array.from(Array(oldBlockChildrenLength - currentBlock.children.length)).forEach( - (_, index) => { - const childIndex = oldBlockChildrenLength - 1 - index - if (childIndex > 0) { - debug('Removing child') - Transforms.removeNodes(slateEditor, { - at: [currentBlockIndex, childIndex], - }) - } - }, - ) - } - currentBlock.children.forEach((currentBlockChild, currentBlockChildIndex) => { - const oldBlockChild = oldBlock.children[currentBlockChildIndex] - const isChildChanged = !isEqual(currentBlockChild, oldBlockChild) - const isTextChanged = !isEqual(currentBlockChild.text, oldBlockChild?.text) - const path = [currentBlockIndex, currentBlockChildIndex] - if (isChildChanged) { - // Update if this is the same child - if (currentBlockChild._key === oldBlockChild?._key) { - debug('Updating changed child', currentBlockChild, oldBlockChild) - Transforms.setNodes(slateEditor, currentBlockChild as Partial, { - at: path, - }) - const isSpanNode = - Text.isText(currentBlockChild) && - currentBlockChild._type === 'span' && - Text.isText(oldBlockChild) && - oldBlockChild._type === 'span' - if (isSpanNode && isTextChanged) { - Transforms.delete(slateEditor, { - at: {focus: {path, offset: 0}, anchor: {path, offset: oldBlockChild.text.length}}, - }) - Transforms.insertText(slateEditor, currentBlockChild.text, { - at: path, - }) - slateEditor.onChange() - } else if (!isSpanNode) { - // If it's a inline block, also update the void text node key - debug('Updating changed inline object child', currentBlockChild) - Transforms.setNodes( - slateEditor, - {_key: VOID_CHILD_KEY}, - { - at: [...path, 0], - voids: true, - }, - ) - } - // Replace the child if _key's are different - } else if (oldBlockChild) { - debug('Replacing child', currentBlockChild) - Transforms.removeNodes(slateEditor, { - at: [currentBlockIndex, currentBlockChildIndex], - }) - withPreserveKeys(slateEditor, () => { - Transforms.insertNodes(slateEditor, currentBlockChild as Node, { - at: [currentBlockIndex, currentBlockChildIndex], - }) - }) - slateEditor.onChange() - // Insert it if it didn't exist before - } else if (!oldBlockChild) { - debug('Inserting new child', currentBlockChild) - withPreserveKeys(slateEditor, () => { - Transforms.insertNodes(slateEditor, currentBlockChild as Node, { - at: [currentBlockIndex, currentBlockChildIndex], - }) - slateEditor.onChange() - }) - } - } - }) - } -} diff --git a/packages/@sanity/portable-text-editor/src/editor/nodes/DefaultAnnotation.tsx b/packages/@sanity/portable-text-editor/src/editor/nodes/DefaultAnnotation.tsx deleted file mode 100644 index 40062594492..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/nodes/DefaultAnnotation.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import {type PortableTextObject} from '@sanity/types' -import {type ReactNode, useCallback} from 'react' - -type Props = { - annotation: PortableTextObject - children: ReactNode -} -export function DefaultAnnotation(props: Props) { - // eslint-disable-next-line no-alert - const handleClick = useCallback(() => alert(JSON.stringify(props.annotation)), [props.annotation]) - return ( - - {props.children} - - ) -} diff --git a/packages/@sanity/portable-text-editor/src/editor/nodes/DefaultObject.tsx b/packages/@sanity/portable-text-editor/src/editor/nodes/DefaultObject.tsx deleted file mode 100644 index cf26926a53a..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/nodes/DefaultObject.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import {type PortableTextBlock, type PortableTextChild} from '@sanity/types' - -type Props = { - value: PortableTextBlock | PortableTextChild -} - -const DefaultObject = (props: Props): JSX.Element => { - return ( -
    -
    {JSON.stringify(props.value, null, 2)}
    -
    - ) -} - -export default DefaultObject diff --git a/packages/@sanity/portable-text-editor/src/editor/nodes/index.ts b/packages/@sanity/portable-text-editor/src/editor/nodes/index.ts deleted file mode 100644 index acf9f0c5a36..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/nodes/index.ts +++ /dev/null @@ -1,189 +0,0 @@ -import {styled} from 'styled-components' - -export const DefaultBlockObject = styled.div<{selected: boolean}>` - user-select: none; - border: ${(props) => { - if (props.selected) { - return '1px solid blue' - } - return '1px solid transparent' - }}; -` - -export const DefaultInlineObject = styled.span<{selected: boolean}>` - background: #999; - border: ${(props) => { - if (props.selected) { - return '1px solid blue' - } - return '1px solid transparent' - }}; -` - -type ListItemProps = {listLevel: number; listStyle: string} - -export const DefaultListItem = styled.div` - &.pt-list-item { - width: fit-content; - position: relative; - display: block; - - /* Important 'transform' in order to force refresh the ::before and ::after rules - in Webkit: https://stackoverflow.com/a/21947628/831480 - */ - transform: translateZ(0); - margin-left: ${(props: ListItemProps) => getLeftPositionForListLevel(props.listLevel)}; - } - &.pt-list-item > .pt-list-item-inner { - display: flex; - margin: 0; - padding: 0; - &:before { - justify-content: flex-start; - vertical-align: top; - } - } - &.pt-list-item-bullet > .pt-list-item-inner:before { - content: '${(props: ListItemProps) => - getContentForListLevelAndStyle(props.listLevel, props.listStyle)}'; - font-size: 0.4375rem; /* 7px */ - line-height: 1.5rem; /* Same as body text */ - /* Optical alignment */ - position: relative; - } - } - &.pt-list-item-bullet > .pt-list-item-inner { - &:before { - min-width: 1.5rem; /* Make sure space between bullet and text never shrinks */ - } - } - &.pt-list-item-number { - counter-increment: ${(props: {listLevel: number}) => - getCounterIncrementForListLevel(props.listLevel)}; - counter-reset: ${(props: {listLevel: number}) => getCounterResetForListLevel(props.listLevel)}; - } - & + :not(.pt-list-item-number) { - counter-reset: listItemNumber; - } - &.pt-list-item-number > .pt-list-item-inner:before { - content: ${(props) => getCounterContentForListLevel(props.listLevel)}; - min-width: 1.5rem; /* Make sure space between number and text never shrinks */ - /* Optical alignment */ - position: relative; - top: 1px; - } -` - -export const DefaultListItemInner = styled.div`` - -function getLeftPositionForListLevel(level: number) { - switch (Number(level)) { - case 1: - return '1.5em' - case 2: - return '3em' - case 3: - return '4.5em' - case 4: - return '6em' - case 5: - return '7.5em' - case 6: - return '9em' - case 7: - return '10.5em' - case 8: - return '12em' - case 9: - return '13.5em' - case 10: - return '15em' - default: - return '0em' - } -} - -const bullets = ['●', '○', '■'] - -function getContentForListLevelAndStyle(level: number, style: string) { - const normalizedLevel = (level - 1) % 3 - if (style === 'bullet') { - return bullets[normalizedLevel] - } - return '*' -} - -function getCounterIncrementForListLevel(level: number) { - switch (level) { - case 1: - return 'listItemNumber' - case 2: - return 'listItemAlpha' - case 3: - return 'listItemRoman' - case 4: - return 'listItemNumberNext' - case 5: - return 'listItemLetterNext' - case 6: - return 'listItemRomanNext' - case 7: - return 'listItemNumberNextNext' - case 8: - return 'listItemAlphaNextNext' - case 9: - return 'listItemRomanNextNext' - default: - return 'listItemNumberNextNextNext' - } -} - -function getCounterResetForListLevel(level: number) { - switch (level) { - case 1: - return 'listItemAlpha' - case 2: - return 'listItemRoman' - case 3: - return 'listItemNumberNext' - case 4: - return 'listItemLetterNext' - case 5: - return 'listItemRomanNext' - case 6: - return 'listItemNumberNextNext' - case 7: - return 'listItemAlphaNextNext' - case 8: - return 'listItemRomanNextNext' - case 9: - return 'listItemNumberNextNextNext' - default: - return 'listItemNumberNextNextNext' - } -} - -function getCounterContentForListLevel(level: number) { - switch (level) { - case 1: - return `counter(listItemNumber) '. '` - case 2: - return `counter(listItemAlpha, lower-alpha) '. '` - case 3: - return `counter(listItemRoman, lower-roman) '. '` - case 4: - return `counter(listItemNumberNext) '. '` - case 5: - return `counter(listItemLetterNext, lower-alpha) '. '` - case 6: - return `counter(listItemRomanNext, lower-roman) '. '` - case 7: - return `counter(listItemNumberNextNext) '. '` - case 8: - return `counter(listItemAlphaNextNext, lower-alpha) '. '` - case 9: - return `counter(listItemRomanNextNext, lower-roman) '. '` - default: - return `counter(listItemNumberNextNextNext) '. '` - } -} diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withEditableAPIDelete.test.tsx b/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withEditableAPIDelete.test.tsx deleted file mode 100644 index 0ad69e82f69..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withEditableAPIDelete.test.tsx +++ /dev/null @@ -1,244 +0,0 @@ -import {describe, expect, it, jest} from '@jest/globals' -import {render, waitFor} from '@testing-library/react' -import {createRef, type RefObject} from 'react' - -import {PortableTextEditorTester, schemaType} from '../../__tests__/PortableTextEditorTester' -import {PortableTextEditor} from '../../PortableTextEditor' - -const initialValue = [ - { - _key: 'a', - _type: 'myTestBlockType', - children: [ - { - _key: 'a1', - _type: 'span', - marks: [], - text: 'Block A', - }, - ], - markDefs: [], - style: 'normal', - }, - { - _key: 'b', - _type: 'myTestBlockType', - children: [ - { - _key: 'b1', - _type: 'span', - marks: [], - text: 'Block B', - }, - ], - markDefs: [], - style: 'normal', - }, -] - -const initialSelection = { - focus: {path: [{_key: 'b'}, 'children', {_key: 'b1'}], offset: 7}, - anchor: {path: [{_key: 'b'}, 'children', {_key: 'b1'}], offset: 7}, -} - -describe('plugin:withEditableAPI: .delete()', () => { - it('deletes block', async () => { - const editorRef: RefObject = createRef() - const onChange = jest.fn() - render( - , - ) - await waitFor(() => { - if (editorRef.current) { - PortableTextEditor.focus(editorRef.current) - PortableTextEditor.select(editorRef.current, initialSelection) - PortableTextEditor.delete( - editorRef.current, - PortableTextEditor.getSelection(editorRef.current), - {mode: 'blocks'}, - ) - expect(PortableTextEditor.getValue(editorRef.current)).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "a", - "_type": "myTestBlockType", - "children": Array [ - Object { - "_key": "a1", - "_type": "span", - "marks": Array [], - "text": "Block A", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - } - }) - }) - - it('deletes all the blocks, but leaves a placeholder block', async () => { - const editorRef: RefObject = createRef() - const onChange = jest.fn() - render( - , - ) - await waitFor(() => { - expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue}) - expect(onChange).toHaveBeenCalledWith({type: 'ready'}) - }) - - await waitFor(() => { - if (editorRef.current) { - PortableTextEditor.delete( - editorRef.current, - { - focus: {path: [{_key: 'b'}, 'children', {_key: 'b1'}], offset: 7}, - anchor: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 0}, - }, - {mode: 'blocks'}, - ) - } - }) - await waitFor(() => { - if (editorRef.current) { - // New keys here confirms that a placeholder block has been created - expect(PortableTextEditor.getValue(editorRef.current)).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "1", - "_type": "myTestBlockType", - "children": Array [ - Object { - "_key": "2", - "_type": "span", - "marks": Array [], - "text": "", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - } - }) - }) - - it('deletes children', async () => { - const editorRef: RefObject = createRef() - const onChange = jest.fn() - render( - , - ) - - await waitFor(() => { - if (editorRef.current) { - PortableTextEditor.select(editorRef.current, { - focus: {path: [{_key: 'b'}, 'children', {_key: 'b1'}], offset: 5}, - anchor: {path: [{_key: 'b'}, 'children', {_key: 'b1'}], offset: 7}, - }) - PortableTextEditor.focus(editorRef.current) - PortableTextEditor.delete( - editorRef.current, - PortableTextEditor.getSelection(editorRef.current), - {mode: 'children'}, - ) - expect(PortableTextEditor.getValue(editorRef.current)).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "a", - "_type": "myTestBlockType", - "children": Array [ - Object { - "_key": "a1", - "_type": "span", - "marks": Array [], - "text": "Block A", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - Object { - "_key": "b", - "_type": "myTestBlockType", - "children": Array [ - Object { - "_key": "1", - "_type": "span", - "marks": Array [], - "text": "", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - } - }) - }) - it('deletes selected', async () => { - const editorRef: RefObject = createRef() - const onChange = jest.fn() - render( - , - ) - - await waitFor(() => { - if (editorRef.current) { - PortableTextEditor.select(editorRef.current, { - focus: {path: [{_key: 'b'}, 'children', {_key: 'b1'}], offset: 5}, - anchor: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 0}, - }) - PortableTextEditor.focus(editorRef.current) - PortableTextEditor.delete( - editorRef.current, - PortableTextEditor.getSelection(editorRef.current), - {mode: 'selected'}, - ) - expect(PortableTextEditor.getValue(editorRef.current)).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "b", - "_type": "myTestBlockType", - "children": Array [ - Object { - "_key": "b1", - "_type": "span", - "marks": Array [], - "text": " B", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - } - }) - }) -}) diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withEditableAPIGetFragment.test.tsx b/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withEditableAPIGetFragment.test.tsx deleted file mode 100644 index 81572ada7c7..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withEditableAPIGetFragment.test.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import {describe, expect, it, jest} from '@jest/globals' -import {isPortableTextTextBlock} from '@sanity/types' -import {render, waitFor} from '@testing-library/react' -import {createRef, type RefObject} from 'react' - -import {PortableTextEditorTester, schemaType} from '../../__tests__/PortableTextEditorTester' -import {PortableTextEditor} from '../../PortableTextEditor' - -const initialValue = [ - { - _key: 'a', - _type: 'myTestBlockType', - children: [ - { - _key: 'a1', - _type: 'span', - marks: [], - text: 'Block A', - }, - ], - markDefs: [], - style: 'normal', - }, - { - _key: 'b', - _type: 'myTestBlockType', - children: [ - { - _key: 'b1', - _type: 'span', - marks: [], - text: 'Block B ', - }, - { - _key: 'b2', - _type: 'someObject', - }, - { - _key: 'b3', - _type: 'span', - marks: [], - text: ' contains a inline object', - }, - ], - markDefs: [], - style: 'normal', - }, -] - -describe('plugin:withEditableAPI: .getFragment()', () => { - it('can get a Portable Text fragment of the current selection in a single block', async () => { - const editorRef: RefObject = createRef() - const onChange = jest.fn() - render( - , - ) - const initialSelection = { - focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 6}, - anchor: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 7}, - } - await waitFor(() => { - if (editorRef.current) { - PortableTextEditor.focus(editorRef.current) - PortableTextEditor.select(editorRef.current, initialSelection) - const fragment = PortableTextEditor.getFragment(editorRef.current) - expect( - fragment && isPortableTextTextBlock(fragment[0]) && fragment[0]?.children[0]?.text, - ).toBe('A') - } - }) - }) - it('can get a Portable Text fragment of the current selection in multiple blocks', async () => { - const editorRef: RefObject = createRef() - const onChange = jest.fn() - render( - , - ) - const initialSelection = { - anchor: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 6}, - focus: {path: [{_key: 'b'}, 'children', {_key: 'b3'}], offset: 9}, - } - await waitFor(() => { - if (editorRef.current) { - PortableTextEditor.focus(editorRef.current) - PortableTextEditor.select(editorRef.current, initialSelection) - const fragment = PortableTextEditor.getFragment(editorRef.current) - expect(fragment).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "a", - "_type": "myTestBlockType", - "children": Array [ - Object { - "_key": "a1", - "_type": "span", - "marks": Array [], - "text": "A", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - Object { - "_key": "b", - "_type": "myTestBlockType", - "children": Array [ - Object { - "_key": "b1", - "_type": "span", - "marks": Array [], - "text": "Block B ", - }, - Object { - "_key": "b2", - "_type": "someObject", - }, - Object { - "_key": "b3", - "_type": "span", - "marks": Array [], - "text": " contains", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - } - }) - }) -}) diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withEditableAPIInsert.test.tsx b/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withEditableAPIInsert.test.tsx deleted file mode 100644 index a0483c52011..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withEditableAPIInsert.test.tsx +++ /dev/null @@ -1,346 +0,0 @@ -import {describe, expect, it, jest} from '@jest/globals' -import {render, waitFor} from '@testing-library/react' -import {createRef, type RefObject} from 'react' - -import {PortableTextEditorTester, schemaType} from '../../__tests__/PortableTextEditorTester' -import {PortableTextEditor} from '../../PortableTextEditor' - -const initialValue = [ - { - _key: 'a', - _type: 'myTestBlockType', - children: [ - { - _key: 'a1', - _type: 'span', - marks: [], - text: 'Block A', - }, - ], - markDefs: [], - style: 'normal', - }, -] -const initialSelection = { - focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 7}, - anchor: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 7}, -} - -const emptyTextBlock = [ - { - _key: 'emptyBlock', - _type: 'myTestBlockType', - children: [ - { - _key: 'emptySpan', - _type: 'span', - marks: [], - text: '', - }, - ], - markDefs: [], - style: 'normal', - }, -] -const emptyBlockSelection = { - focus: {path: [{_key: 'emptyBlock'}, 'children', {_key: 'emptySpan'}], offset: 0}, - anchor: {path: [{_key: 'emptyBlock'}, 'children', {_key: 'emptySpan'}], offset: 0}, -} - -describe('plugin:withEditableAPI: .insertChild()', () => { - it('inserts child nodes correctly', async () => { - const editorRef: RefObject = createRef() - const onChange = jest.fn() - render( - , - ) - const editor = editorRef.current - const inlineType = editor?.schemaTypes.inlineObjects.find((t) => t.name === 'someObject') - await waitFor(() => { - if (editor && inlineType) { - PortableTextEditor.focus(editor) - PortableTextEditor.select(editor, initialSelection) - PortableTextEditor.insertChild(editorRef.current, inlineType, {color: 'red'}) - expect(PortableTextEditor.getValue(editorRef.current)).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "a", - "_type": "myTestBlockType", - "children": Array [ - Object { - "_key": "a1", - "_type": "span", - "marks": Array [], - "text": "Block A", - }, - Object { - "_key": "3", - "_type": "someObject", - "color": "red", - }, - Object { - "_key": "4", - "_type": "span", - "marks": Array [], - "text": "", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - PortableTextEditor.insertChild(editor, editor.schemaTypes.span, {text: ' '}) - expect(PortableTextEditor.getValue(editor)).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "a", - "_type": "myTestBlockType", - "children": Array [ - Object { - "_key": "a1", - "_type": "span", - "marks": Array [], - "text": "Block A", - }, - Object { - "_key": "3", - "_type": "someObject", - "color": "red", - }, - Object { - "_key": "7", - "_type": "span", - "marks": Array [], - "text": " ", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - const sel = PortableTextEditor.getSelection(editor) - expect(sel).toMatchInlineSnapshot(` - Object { - "anchor": Object { - "offset": 1, - "path": Array [ - Object { - "_key": "a", - }, - "children", - Object { - "_key": "7", - }, - ], - }, - "backward": false, - "focus": Object { - "offset": 1, - "path": Array [ - Object { - "_key": "a", - }, - "children", - Object { - "_key": "7", - }, - ], - }, - } - `) - } - }) - }) -}) - -describe('plugin:withEditableAPI: .insertBlock()', () => { - it('should not add empty blank blocks: empty block', async () => { - const editorRef: RefObject = createRef() - const onChange = jest.fn() - render( - , - ) - const editor = editorRef.current - const someObject = editor?.schemaTypes.inlineObjects.find((t) => t.name === 'someObject') - - await waitFor(() => { - if (editorRef.current && someObject) { - PortableTextEditor.focus(editorRef.current) - PortableTextEditor.select(editorRef.current, emptyBlockSelection) - PortableTextEditor.insertBlock(editorRef.current, someObject, {color: 'red'}) - - expect(PortableTextEditor.getValue(editorRef.current)).toEqual([ - {_key: '2', _type: 'someObject', color: 'red'}, - ]) - } else { - throw new Error('No editor or someObject') - } - }) - }) - - it('should not add empty blank blocks: non-empty block', async () => { - const editorRef: RefObject = createRef() - const onChange = jest.fn() - render( - , - ) - const editor = editorRef.current - const someObject = editor?.schemaTypes.inlineObjects.find((t) => t.name === 'someObject') - - await waitFor(() => { - if (editorRef.current && someObject) { - PortableTextEditor.focus(editorRef.current) - PortableTextEditor.select(editorRef.current, initialSelection) - PortableTextEditor.insertBlock(editorRef.current, someObject, {color: 'red'}) - expect(PortableTextEditor.getValue(editorRef.current)).toEqual([ - ...initialValue, - {_key: '2', _type: 'someObject', color: 'red'}, - ]) - } else { - throw new Error('No editor or someObject') - } - }) - }) - it('should be inserted before if focus is on start of block', async () => { - const editorRef: RefObject = createRef() - const onChange = jest.fn() - render( - , - ) - const editor = editorRef.current - const someObject = editor?.schemaTypes.inlineObjects.find((t) => t.name === 'someObject') - - await waitFor(() => { - if (editorRef.current && someObject) { - PortableTextEditor.focus(editorRef.current) - PortableTextEditor.select(editorRef.current, { - focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 0}, - anchor: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 0}, - }) - PortableTextEditor.insertBlock(editorRef.current, someObject, {color: 'red'}) - expect(PortableTextEditor.getValue(editorRef.current)).toEqual([ - {_key: '2', _type: 'someObject', color: 'red'}, - ...initialValue, - ]) - } else { - throw new Error('No editor or someObject') - } - }) - }) - it('should not add empty blank blocks: non text block', async () => { - const editorRef: RefObject = createRef() - const onChange = jest.fn() - const value = [...initialValue, {_key: 'b', _type: 'someObject', color: 'red'}] - render( - , - ) - const editor = editorRef.current - const someObject = editor?.schemaTypes.inlineObjects.find((t) => t.name === 'someObject') - - await waitFor(() => { - if (editorRef.current && someObject) { - PortableTextEditor.focus(editorRef.current) - // Focus the `someObject` block - PortableTextEditor.select(editorRef.current, { - focus: {path: [{_key: 'b'}], offset: 0}, - anchor: {path: [{_key: 'b'}], offset: 0}, - }) - PortableTextEditor.insertBlock(editorRef.current, someObject, {color: 'yellow'}) - expect(PortableTextEditor.getValue(editorRef.current)).toEqual([ - ...value, - {_key: '2', _type: 'someObject', color: 'yellow'}, - ]) - } else { - throw new Error('No editor or someObject') - } - }) - }) - it('should not add empty blank blocks: in between blocks', async () => { - const editorRef: RefObject = createRef() - const onChange = jest.fn() - const value = [...initialValue, {_key: 'b', _type: 'someObject', color: 'red'}] - render( - , - ) - const editor = editorRef.current - const someObject = editor?.schemaTypes.inlineObjects.find((t) => t.name === 'someObject') - - await waitFor(() => { - if (editorRef.current && someObject) { - PortableTextEditor.focus(editorRef.current) - // Focus the `text` block - PortableTextEditor.select(editorRef.current, initialSelection) - PortableTextEditor.insertBlock(editorRef.current, someObject, {color: 'yellow'}) - expect(PortableTextEditor.getValue(editorRef.current)).toEqual([ - value[0], - {_key: '2', _type: 'someObject', color: 'yellow'}, - value[1], - ]) - } else { - throw new Error('No editor or someObject') - } - }) - }) - it('should not add empty blank blocks: in new empty text block', async () => { - const editorRef: RefObject = createRef() - const onChange = jest.fn() - const value = [...initialValue, ...emptyTextBlock] - render( - , - ) - const editor = editorRef.current - const someObject = editor?.schemaTypes.inlineObjects.find((t) => t.name === 'someObject') - - await waitFor(() => { - if (editorRef.current && someObject) { - PortableTextEditor.focus(editorRef.current) - // Focus the empty `text` block - PortableTextEditor.select(editorRef.current, emptyBlockSelection) - PortableTextEditor.insertBlock(editorRef.current, someObject, {color: 'yellow'}) - expect(PortableTextEditor.getValue(editorRef.current)).toEqual([ - value[0], - {_key: '2', _type: 'someObject', color: 'yellow'}, - ]) - } else { - throw new Error('No editor or someObject') - } - }) - }) -}) diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withEditableAPISelectionsOverlapping.test.tsx b/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withEditableAPISelectionsOverlapping.test.tsx deleted file mode 100644 index 0dcb4d27f38..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withEditableAPISelectionsOverlapping.test.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import {describe, expect, it, jest} from '@jest/globals' -import {type PortableTextBlock} from '@sanity/types' -import {render, waitFor} from '@testing-library/react' -import {createRef, type RefObject} from 'react' - -import {PortableTextEditorTester, schemaType} from '../../__tests__/PortableTextEditorTester' -import {PortableTextEditor} from '../../PortableTextEditor' - -const INITIAL_VALUE: PortableTextBlock[] = [ - { - _key: 'a', - _type: 'myTestBlockType', - children: [ - { - _key: 'a1', - _type: 'span', - marks: [], - text: 'This is some text in the block', - }, - ], - markDefs: [], - style: 'normal', - }, -] - -describe('plugin:withEditableAPI: .isSelectionsOverlapping', () => { - it('returns true if the selections are partially overlapping', async () => { - const editorRef: RefObject = createRef() - const onChange = jest.fn() - render( - , - ) - const selectionA = { - focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 4}, - anchor: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 8}, - } - - const selectionB = { - focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 2}, - anchor: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 6}, - } - - await waitFor(() => { - if (editorRef.current) { - const isOverlapping = PortableTextEditor.isSelectionsOverlapping( - editorRef.current, - selectionA, - selectionB, - ) - - expect(isOverlapping).toBe(true) - } - }) - }) - - it('returns true if the selections are fully overlapping', async () => { - const editorRef: RefObject = createRef() - const onChange = jest.fn() - render( - , - ) - const selectionA = { - focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 4}, - anchor: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 8}, - } - - const selectionB = { - focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 4}, - anchor: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 8}, - } - - await waitFor(() => { - if (editorRef.current) { - const isOverlapping = PortableTextEditor.isSelectionsOverlapping( - editorRef.current, - selectionA, - selectionB, - ) - - expect(isOverlapping).toBe(true) - } - }) - }) - - it('return true if selection is fully inside another selection', async () => { - const editorRef: RefObject = createRef() - const onChange = jest.fn() - render( - , - ) - const selectionA = { - focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 2}, - anchor: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 10}, - } - - const selectionB = { - focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 4}, - anchor: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 6}, - } - - await waitFor(() => { - if (editorRef.current) { - const isOverlapping = PortableTextEditor.isSelectionsOverlapping( - editorRef.current, - selectionA, - selectionB, - ) - - expect(isOverlapping).toBe(true) - } - }) - }) - - it('returns false if the selections are not overlapping', async () => { - const editorRef: RefObject = createRef() - const onChange = jest.fn() - render( - , - ) - const selectionA = { - focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 4}, - anchor: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 8}, - } - - const selectionB = { - focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 10}, - anchor: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 12}, - } - - await waitFor(() => { - if (editorRef.current) { - const isOverlapping = PortableTextEditor.isSelectionsOverlapping( - editorRef.current, - selectionA, - selectionB, - ) - - expect(isOverlapping).toBe(false) - } - }) - }) -}) diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withHotkeys.test.tsx b/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withHotkeys.test.tsx deleted file mode 100644 index 69c737ec8cb..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withHotkeys.test.tsx +++ /dev/null @@ -1,212 +0,0 @@ -import {describe, expect, it, jest} from '@jest/globals' -import {render, waitFor} from '@testing-library/react' -import {createRef, type RefObject} from 'react' - -import {PortableTextEditorTester, schemaType} from '../../__tests__/PortableTextEditorTester' -import {getEditableElement, triggerKeyboardEvent} from '../../__tests__/utils' -import {PortableTextEditor} from '../../PortableTextEditor' - -const newBlock = { - _type: 'myTestBlockType', - _key: '3', - style: 'normal', - markDefs: [], - children: [ - { - _type: 'span', - _key: '2', - text: '', - marks: [], - }, - ], -} -describe('plugin:withHotkeys: .ArrowDown', () => { - it('a new block is added if the user is focused on the only block which is void, and presses arrow down.', async () => { - const initialValue = [ - { - _key: 'a', - _type: 'someObject', - }, - ] - - const initialSelection = { - focus: {path: [{_key: 'a'}], offset: 0}, - anchor: {path: [{_key: 'a'}], offset: 0}, - } - - const editorRef: RefObject = createRef() - const onChange = jest.fn() - const component = render( - , - ) - const element = await getEditableElement(component) - - const editor = editorRef.current - const inlineType = editor?.schemaTypes.inlineObjects.find((t) => t.name === 'someObject') - await waitFor(async () => { - if (editor && inlineType && element) { - PortableTextEditor.focus(editor) - PortableTextEditor.select(editor, initialSelection) - PortableTextEditor.insertBreak(editor) - await triggerKeyboardEvent('ArrowDown', element) - - const value = PortableTextEditor.getValue(editor) - expect(value).toEqual([initialValue[0], newBlock]) - } - }) - }) - it('a new block is added if the user is focused on the last block which is void, and presses arrow down.', async () => { - const initialValue = [ - { - _type: 'myTestBlockType', - _key: 'a', - style: 'normal', - markDefs: [], - children: [ - { - _type: 'span', - _key: 'a1', - text: 'This is the first block', - marks: [], - }, - ], - }, - { - _key: 'b', - _type: 'someObject', - }, - ] - const initialSelection = { - focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 2}, - anchor: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 2}, - } - - const editorRef: RefObject = createRef() - const onChange = jest.fn() - const component = render( - , - ) - const element = await getEditableElement(component) - - const editor = editorRef.current - const inlineType = editor?.schemaTypes.inlineObjects.find((t) => t.name === 'someObject') - await waitFor(async () => { - if (editor && inlineType && element) { - PortableTextEditor.focus(editor) - PortableTextEditor.select(editor, initialSelection) - await triggerKeyboardEvent('ArrowDown', element) - const value = PortableTextEditor.getValue(editor) - // Arrow down on the text block should not add a new block - expect(value).toEqual(initialValue) - // Focus on the object block - PortableTextEditor.select(editor, { - focus: {path: [{_key: 'b'}], offset: 0}, - anchor: {path: [{_key: 'b'}], offset: 0}, - }) - await triggerKeyboardEvent('ArrowDown', element) - const value2 = PortableTextEditor.getValue(editor) - expect(value2).toEqual([ - initialValue[0], - initialValue[1], - { - _type: 'myTestBlockType', - _key: '3', - style: 'normal', - markDefs: [], - children: [ - { - _type: 'span', - _key: '2', - text: '', - marks: [], - }, - ], - }, - ]) - } - }) - }) -}) -describe('plugin:withHotkeys: .ArrowUp', () => { - it('a new block is added at the top, when pressing arrow up, because first block is void, the new block can be deleted with backspace.', async () => { - const initialValue = [ - { - _key: 'b', - _type: 'someObject', - }, - { - _type: 'myTestBlockType', - _key: 'a', - style: 'normal', - markDefs: [], - children: [ - { - _type: 'span', - _key: 'a1', - text: 'This is the first block', - marks: [], - }, - ], - }, - ] - - const initialSelection = { - focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 2}, - anchor: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 2}, - } - - const editorRef: RefObject = createRef() - const onChange = jest.fn() - const component = render( - , - ) - const element = await getEditableElement(component) - - const editor = editorRef.current - const inlineType = editor?.schemaTypes.inlineObjects.find((t) => t.name === 'someObject') - await waitFor(async () => { - if (editor && inlineType && element) { - PortableTextEditor.focus(editor) - PortableTextEditor.select(editor, initialSelection) - await triggerKeyboardEvent('ArrowUp', element) - // Arrow down on the text block should not add a new block - expect(PortableTextEditor.getValue(editor)).toEqual(initialValue) - // Focus on the object block - PortableTextEditor.select(editor, { - focus: {path: [{_key: 'b'}], offset: 0}, - anchor: {path: [{_key: 'b'}], offset: 0}, - }) - await triggerKeyboardEvent('ArrowUp', element) - expect(PortableTextEditor.getValue(editor)).toEqual([ - newBlock, - initialValue[0], - initialValue[1], - ]) - // Pressing arrow up again won't add a new block - await triggerKeyboardEvent('ArrowUp', element) - expect(PortableTextEditor.getValue(editor)).toEqual([ - newBlock, - initialValue[0], - initialValue[1], - ]) - await triggerKeyboardEvent('Backspace', element) - expect(PortableTextEditor.getValue(editor)).toEqual(initialValue) - } - }) - }) -}) diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withInsertBreak.test.tsx b/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withInsertBreak.test.tsx deleted file mode 100644 index 488a2b04f30..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withInsertBreak.test.tsx +++ /dev/null @@ -1,204 +0,0 @@ -import {describe, expect, it, jest} from '@jest/globals' -import {render, waitFor} from '@testing-library/react' -import {createRef, type RefObject} from 'react' - -import {PortableTextEditorTester, schemaType} from '../../__tests__/PortableTextEditorTester' -import {PortableTextEditor} from '../../PortableTextEditor' - -const initialValue = [ - { - _key: 'a', - _type: 'myTestBlockType', - children: [ - { - _key: 'a1', - _type: 'span', - marks: [], - text: 'Block A', - }, - ], - markDefs: [], - style: 'normal', - }, - { - _key: 'b', - _type: 'myTestBlockType', - children: [ - { - _key: 'b1', - _type: 'span', - marks: [], - text: 'Block B', - }, - ], - markDefs: [], - style: 'normal', - }, -] - -describe('plugin:withInsertBreak: "enter"', () => { - it('keeps text block key if enter is pressed at the start of the block, creating a new one in "before" position', async () => { - const initialSelection = { - focus: {path: [{_key: 'b'}, 'children', {_key: 'b1'}], offset: 0}, - anchor: {path: [{_key: 'b'}, 'children', {_key: 'b1'}], offset: 0}, - } - - const editorRef: RefObject = createRef() - const onChange = jest.fn() - render( - , - ) - const editor = editorRef.current - const inlineType = editor?.schemaTypes.inlineObjects.find((t) => t.name === 'someObject') - await waitFor(async () => { - if (editor && inlineType) { - PortableTextEditor.focus(editor) - PortableTextEditor.select(editor, initialSelection) - PortableTextEditor.insertBreak(editor) - - const value = PortableTextEditor.getValue(editor) - expect(value).toEqual([ - initialValue[0], - { - _type: 'myTestBlockType', - _key: '3', - style: 'normal', - markDefs: [], - children: [ - { - _type: 'span', - _key: '2', - text: '', - marks: [], - }, - ], - }, - initialValue[1], - ]) - } - }) - }) - it('inserts the new block after if key enter is pressed at the start of the block, creating a new one in "after" position if the block is empty', async () => { - const initialSelection = { - focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 0}, - anchor: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 0}, - } - const emptyBlock = { - _key: 'a', - _type: 'myTestBlockType', - children: [ - { - _key: 'a1', - _type: 'span', - marks: [], - text: '', - }, - ], - markDefs: [], - style: 'normal', - } - - const editorRef: RefObject = createRef() - const onChange = jest.fn() - render( - , - ) - const editor = editorRef.current - const inlineType = editor?.schemaTypes.inlineObjects.find((t) => t.name === 'someObject') - await waitFor(async () => { - if (editor && inlineType) { - PortableTextEditor.focus(editor) - PortableTextEditor.select(editor, initialSelection) - PortableTextEditor.insertBreak(editor) - - const value = PortableTextEditor.getValue(editor) - expect(value).toEqual([ - emptyBlock, - { - _key: '2', - _type: 'myTestBlockType', - markDefs: [], - style: 'normal', - children: [ - { - _key: '1', - _type: 'span', - marks: [], - text: '', - }, - ], - }, - ]) - } - }) - }) - it('splits the text block key if enter is pressed at the middle of the block', async () => { - const initialSelection = { - focus: {path: [{_key: 'b'}, 'children', {_key: 'b1'}], offset: 2}, - anchor: {path: [{_key: 'b'}, 'children', {_key: 'b1'}], offset: 2}, - } - - const editorRef: RefObject = createRef() - const onChange = jest.fn() - render( - , - ) - const editor = editorRef.current - const inlineType = editor?.schemaTypes.inlineObjects.find((t) => t.name === 'someObject') - await waitFor(async () => { - if (editor && inlineType) { - PortableTextEditor.focus(editor) - PortableTextEditor.select(editor, initialSelection) - PortableTextEditor.insertBreak(editor) - - const value = PortableTextEditor.getValue(editor) - expect(value).toEqual([ - initialValue[0], - { - _key: 'b', - _type: 'myTestBlockType', - children: [ - { - _key: 'b1', - _type: 'span', - marks: [], - text: 'Bl', - }, - ], - markDefs: [], - style: 'normal', - }, - { - _key: '2', - _type: 'myTestBlockType', - markDefs: [], - style: 'normal', - children: [ - { - _key: '1', - _type: 'span', - marks: [], - text: 'ock B', - }, - ], - }, - ]) - } - }) - }) -}) diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withPlaceholderBlock.test.tsx b/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withPlaceholderBlock.test.tsx deleted file mode 100644 index b50a546f463..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withPlaceholderBlock.test.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import {describe, expect, it, jest} from '@jest/globals' -/* eslint-disable max-nested-callbacks */ -import {render, waitFor} from '@testing-library/react' -import {createRef, type RefObject} from 'react' - -import {PortableTextEditorTester, schemaType} from '../../__tests__/PortableTextEditorTester' -import {PortableTextEditor} from '../../PortableTextEditor' - -describe('plugin:withPlaceholderBlock', () => { - describe('removing nodes', () => { - it("should insert an empty text block if it's removing the only block", async () => { - const editorRef: RefObject = createRef() - const initialValue = [ - { - _key: '5fc57af23597', - _type: 'someObject', - }, - ] - const onChange = jest.fn() - await waitFor(() => { - render( - , - ) - }) - - await waitFor(() => { - if (editorRef.current) { - PortableTextEditor.focus(editorRef.current) - - PortableTextEditor.delete( - editorRef.current, - { - focus: {path: [{_key: '5fc57af23597'}], offset: 0}, - anchor: {path: [{_key: '5fc57af23597'}], offset: 0}, - }, - {mode: 'blocks'}, - ) - - const value = PortableTextEditor.getValue(editorRef.current) - - expect(value).toEqual([ - { - _type: 'myTestBlockType', - _key: '3', - style: 'normal', - markDefs: [], - children: [ - { - _type: 'span', - _key: '2', - text: '', - marks: [], - }, - ], - }, - ]) - } - }) - }) - it('should not insert a new block if we have more blocks available', async () => { - const editorRef: RefObject = createRef() - const initialValue = [ - { - _key: '5fc57af23597', - _type: 'someObject', - }, - { - _type: 'myTestBlockType', - _key: 'existingBlock', - style: 'normal', - markDefs: [], - children: [ - { - _type: 'span', - _key: '2', - text: '', - marks: [], - }, - ], - }, - ] - const onChange = jest.fn() - await waitFor(() => { - render( - , - ) - }) - - await waitFor(() => { - if (editorRef.current) { - PortableTextEditor.focus(editorRef.current) - - PortableTextEditor.delete( - editorRef.current, - { - focus: {path: [{_key: '5fc57af23597'}], offset: 0}, - anchor: {path: [{_key: '5fc57af23597'}], offset: 0}, - }, - {mode: 'blocks'}, - ) - - const value = PortableTextEditor.getValue(editorRef.current) - expect(value).toEqual([ - { - _type: 'myTestBlockType', - _key: 'existingBlock', - style: 'normal', - markDefs: [], - children: [ - { - _type: 'span', - _key: '2', - text: '', - marks: [], - }, - ], - }, - ]) - } - }) - }) - }) -}) diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withPortableTextLists.test.tsx b/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withPortableTextLists.test.tsx deleted file mode 100644 index c833b8cdda9..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withPortableTextLists.test.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import {describe, expect, it, jest} from '@jest/globals' -import {render, waitFor} from '@testing-library/react' -import {createRef, type RefObject} from 'react' - -import {PortableTextEditorTester, schemaType} from '../../__tests__/PortableTextEditorTester' -import {PortableTextEditor} from '../../PortableTextEditor' - -describe('plugin:withPortableTextLists', () => { - it('should return active list styles that cover the whole selection', async () => { - const editorRef: RefObject = createRef() - const initialValue = [ - { - _key: 'a', - _type: 'myTestBlockType', - children: [ - { - _key: 'a1', - _type: 'span', - marks: [], - text: '12', - }, - ], - markDefs: [], - style: 'normal', - }, - { - _key: 'b', - _type: 'myTestBlockType', - children: [ - { - _key: '2', - _type: 'span', - marks: [], - text: '34', - level: 1, - listItem: 'bullet', - }, - ], - markDefs: [], - style: 'normal', - }, - ] - const onChange = jest.fn() - await waitFor(() => { - render( - , - ) - }) - const editor = editorRef.current! - expect(editor).toBeDefined() - await waitFor(() => { - PortableTextEditor.focus(editor) - PortableTextEditor.select(editor, { - focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 0}, - anchor: {path: [{_key: '2'}, 'children', {_key: '2'}], offset: 2}, - }) - expect(PortableTextEditor.hasListStyle(editor, 'bullet')).toBe(false) - }) - }) -}) diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withPortableTextMarkModel.test.tsx b/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withPortableTextMarkModel.test.tsx deleted file mode 100644 index c707fdcf2d6..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withPortableTextMarkModel.test.tsx +++ /dev/null @@ -1,1377 +0,0 @@ -import {describe, expect, it, jest} from '@jest/globals' -/* eslint-disable max-nested-callbacks */ -import {render, waitFor} from '@testing-library/react' -import {createRef, type RefObject} from 'react' - -import {type EditorSelection} from '../../../types/editor' -import { - PortableTextEditorTester, - schemaType, - schemaTypeWithColorAndLink, -} from '../../__tests__/PortableTextEditorTester' -import {PortableTextEditor} from '../../PortableTextEditor' - -describe('plugin:withPortableTextMarksModel', () => { - describe('normalization', () => { - it('merges adjacent spans correctly when removing annotations', async () => { - const editorRef: RefObject = createRef() - const initialValue = [ - { - _key: '5fc57af23597', - _type: 'myTestBlockType', - children: [ - { - _key: 'be1c67c6971a', - _type: 'span', - marks: [], - text: 'This is a ', - }, - { - _key: '11c8c9f783a8', - _type: 'span', - marks: ['fde1fd54b544'], - text: 'link', - }, - { - _key: '576c748e0cd2', - _type: 'span', - marks: [], - text: ', this is ', - }, - { - _key: 'f3d73d3833bf', - _type: 'span', - marks: ['7b6d3d5de30c'], - text: 'another', - }, - { - _key: '73b01f13c2ec', - _type: 'span', - marks: [], - text: ', and this is ', - }, - { - _key: '13eb0d467c82', - _type: 'span', - marks: ['93a1d24eade0'], - text: 'a third', - }, - ], - markDefs: [ - { - _key: 'fde1fd54b544', - _type: 'link', - url: '1', - }, - { - _key: '7b6d3d5de30c', - _type: 'link', - url: '2', - }, - { - _key: '93a1d24eade0', - _type: 'link', - url: '3', - }, - ], - style: 'normal', - }, - ] - - const onChange = jest.fn() - await waitFor(() => { - render( - , - ) - }) - - await waitFor(() => { - if (editorRef.current) { - PortableTextEditor.focus(editorRef.current) - PortableTextEditor.select(editorRef.current, { - focus: {path: [{_key: '5fc57af23597'}, 'children', {_key: '11c8c9f783a8'}], offset: 4}, - anchor: {path: [{_key: '5fc57af23597'}, 'children', {_key: '11c8c9f783a8'}], offset: 0}, - }) - // eslint-disable-next-line max-nested-callbacks - const linkType = editorRef.current.schemaTypes.annotations.find((a) => a.name === 'link') - if (!linkType) { - throw new Error('No link type found') - } - PortableTextEditor.removeAnnotation(editorRef.current, linkType) - expect(PortableTextEditor.getValue(editorRef.current)).toEqual([ - { - _key: '5fc57af23597', - _type: 'myTestBlockType', - children: [ - { - _key: 'be1c67c6971a', - _type: 'span', - marks: [], - text: 'This is a link, this is ', - }, - { - _key: 'f3d73d3833bf', - _type: 'span', - marks: ['7b6d3d5de30c'], - text: 'another', - }, - { - _key: '73b01f13c2ec', - _type: 'span', - marks: [], - text: ', and this is ', - }, - { - _key: '13eb0d467c82', - _type: 'span', - marks: ['93a1d24eade0'], - text: 'a third', - }, - ], - markDefs: [ - { - _key: '7b6d3d5de30c', - _type: 'link', - url: '2', - }, - { - _key: '93a1d24eade0', - _type: 'link', - url: '3', - }, - ], - style: 'normal', - }, - ]) - } - }) - }) - - it('splits correctly when adding marks', async () => { - const editorRef: RefObject = createRef() - const initialValue = [ - { - _key: 'a', - _type: 'myTestBlockType', - children: [ - { - _key: 'a1', - _type: 'span', - marks: [], - text: '123', - }, - ], - markDefs: [], - style: 'normal', - }, - { - _key: 'b', - _type: 'myTestBlockType', - children: [ - { - _key: 'b1', - _type: 'span', - marks: [], - text: '123', - }, - ], - markDefs: [], - style: 'normal', - }, - ] - const onChange = jest.fn() - await waitFor(() => { - render( - , - ) - }) - await waitFor(() => { - if (editorRef.current) { - const editor = editorRef.current - PortableTextEditor.focus(editor) - PortableTextEditor.select(editor, { - focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 0}, - anchor: {path: [{_key: 'b'}, 'children', {_key: 'b1'}], offset: 1}, - }) - PortableTextEditor.toggleMark(editor, 'strong') - const value = PortableTextEditor.getValue(editor) - expect(value).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "a", - "_type": "myTestBlockType", - "children": Array [ - Object { - "_key": "a1", - "_type": "span", - "marks": Array [ - "strong", - ], - "text": "123", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - Object { - "_key": "b", - "_type": "myTestBlockType", - "children": Array [ - Object { - "_key": "b1", - "_type": "span", - "marks": Array [ - "strong", - ], - "text": "1", - }, - Object { - "_key": "1", - "_type": "span", - "marks": Array [], - "text": "23", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - } - }) - }) - it('merges children correctly when toggling marks in various ranges', async () => { - const editorRef: RefObject = createRef() - const initialValue = [ - { - _key: 'a', - _type: 'myTestBlockType', - children: [ - { - _key: 'a1', - _type: 'span', - marks: [], - text: '1234', - }, - ], - markDefs: [], - style: 'normal', - }, - ] - const onChange = jest.fn() - await waitFor(() => { - render( - , - ) - }) - const editor = editorRef.current! - expect(editor).toBeDefined() - await waitFor(() => { - PortableTextEditor.focus(editor) - PortableTextEditor.select(editor, { - focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 0}, - anchor: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 4}, - }) - PortableTextEditor.toggleMark(editor, 'strong') - expect(PortableTextEditor.getValue(editor)).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "a", - "_type": "myTestBlockType", - "children": Array [ - Object { - "_key": "a1", - "_type": "span", - "marks": Array [ - "strong", - ], - "text": "1234", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - }) - await waitFor(() => { - if (editorRef.current) { - PortableTextEditor.select(editorRef.current, { - focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 1}, - anchor: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 3}, - }) - PortableTextEditor.toggleMark(editorRef.current, 'strong') - expect(PortableTextEditor.getValue(editorRef.current)).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "a", - "_type": "myTestBlockType", - "children": Array [ - Object { - "_key": "a1", - "_type": "span", - "marks": Array [ - "strong", - ], - "text": "1", - }, - Object { - "_key": "2", - "_type": "span", - "marks": Array [], - "text": "23", - }, - Object { - "_key": "1", - "_type": "span", - "marks": Array [ - "strong", - ], - "text": "4", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - } - }) - await waitFor(() => { - if (editor) { - PortableTextEditor.select(editor, { - focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 0}, - anchor: {path: [{_key: 'a'}, 'children', {_key: '1'}], offset: 1}, - }) - PortableTextEditor.toggleMark(editor, 'strong') - expect(PortableTextEditor.getValue(editor)).toMatchInlineSnapshot(` -Array [ - Object { - "_key": "a", - "_type": "myTestBlockType", - "children": Array [ - Object { - "_key": "a1", - "_type": "span", - "marks": Array [ - "strong", - ], - "text": "1234", - }, - ], - "markDefs": Array [], - "style": "normal", - }, -] -`) - } - }) - }) - it('toggles marks on children with annotation marks correctly', async () => { - const editorRef: RefObject = createRef() - const initialValue = [ - { - _key: 'a', - _type: 'myTestBlockType', - children: [ - { - _key: 'a1', - _type: 'span', - marks: ['abc'], - text: 'A link', - }, - { - _key: 'a2', - _type: 'span', - marks: [], - text: ', not a link', - }, - ], - markDefs: [ - { - _type: 'link', - _key: 'abc', - href: 'http://www.link.com', - }, - ], - style: 'normal', - }, - ] - const onChange = jest.fn() - await waitFor(() => { - render( - , - ) - }) - const editor = editorRef.current! - expect(editor).toBeDefined() - - await waitFor(() => { - PortableTextEditor.focus(editor) - PortableTextEditor.select(editor, { - focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 0}, - anchor: {path: [{_key: 'a'}, 'children', {_key: 'b1'}], offset: 12}, - }) - PortableTextEditor.toggleMark(editor, 'strong') - expect(PortableTextEditor.getValue(editor)).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "a", - "_type": "myTestBlockType", - "children": Array [ - Object { - "_key": "a1", - "_type": "span", - "marks": Array [ - "abc", - "strong", - ], - "text": "A link", - }, - Object { - "_key": "a2", - "_type": "span", - "marks": Array [ - "strong", - ], - "text": ", not a link", - }, - ], - "markDefs": Array [ - Object { - "_key": "abc", - "_type": "link", - "href": "http://www.link.com", - }, - ], - "style": "normal", - }, - ] - `) - }) - }) - - it('merges blocks correctly when containing links', async () => { - const editorRef: RefObject = createRef() - const initialValue = [ - { - _key: '5fc57af23597', - _type: 'myTestBlockType', - children: [ - { - _key: 'be1c67c6971a', - _type: 'span', - marks: [], - text: 'This is a ', - }, - { - _key: '11c8c9f783a8', - _type: 'span', - marks: ['fde1fd54b544'], - text: 'link', - }, - ], - markDefs: [ - { - _key: 'fde1fd54b544', - _type: 'link', - url: '1', - }, - ], - style: 'normal', - }, - { - _key: '7cd53af36712', - _type: 'myTestBlockType', - children: [ - { - _key: '576c748e0cd2', - _type: 'span', - marks: [], - text: 'This is ', - }, - { - _key: 'f3d73d3833bf', - _type: 'span', - marks: ['7b6d3d5de30c'], - text: 'another', - }, - ], - markDefs: [ - { - _key: '7b6d3d5de30c', - _type: 'link', - url: '2', - }, - ], - style: 'normal', - }, - ] - const sel: EditorSelection = { - focus: {path: [{_key: '5fc57af23597'}, 'children', {_key: '11c8c9f783a8'}], offset: 4}, - anchor: {path: [{_key: '7cd53af36712'}, 'children', {_key: '576c748e0cd2'}], offset: 0}, - } - const onChange = jest.fn() - await waitFor(() => { - render( - , - ) - }) - const editor = editorRef.current! - expect(editor).toBeDefined() - await waitFor(() => { - PortableTextEditor.select(editor, sel) - PortableTextEditor.delete(editor, sel) - expect(PortableTextEditor.getValue(editor)).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "5fc57af23597", - "_type": "myTestBlockType", - "children": Array [ - Object { - "_key": "be1c67c6971a", - "_type": "span", - "marks": Array [], - "text": "This is a ", - }, - Object { - "_key": "11c8c9f783a8", - "_type": "span", - "marks": Array [ - "fde1fd54b544", - ], - "text": "link", - }, - Object { - "_key": "576c748e0cd2", - "_type": "span", - "marks": Array [], - "text": "This is ", - }, - Object { - "_key": "f3d73d3833bf", - "_type": "span", - "marks": Array [ - "7b6d3d5de30c", - ], - "text": "another", - }, - ], - "markDefs": Array [ - Object { - "_key": "fde1fd54b544", - "_type": "link", - "url": "1", - }, - Object { - "_key": "7b6d3d5de30c", - "_type": "link", - "url": "2", - }, - ], - "style": "normal", - }, - ] - `) - }) - }) - - it('resets markDefs when splitting a block in the beginning', async () => { - const editorRef: RefObject = createRef() - const initialValue = [ - { - _key: '1987f99da4a2', - _type: 'myTestBlockType', - children: [ - { - _key: '3693e789451c', - _type: 'span', - marks: [], - text: '1', - }, - ], - markDefs: [], - style: 'normal', - }, - { - _key: '2f55670a03bb', - _type: 'myTestBlockType', - children: [ - { - _key: '9f5ed7dee7ab', - _type: 'span', - marks: ['bab319ad3a9d'], - text: '2', - }, - ], - markDefs: [ - { - _key: 'bab319ad3a9d', - _type: 'link', - href: 'http://www.123.com', - }, - ], - style: 'normal', - }, - ] - const sel: EditorSelection = { - focus: {path: [{_key: '2f55670a03bb'}, 'children', {_key: '9f5ed7dee7ab'}], offset: 0}, - anchor: {path: [{_key: '2f55670a03bb'}, 'children', {_key: '9f5ed7dee7ab'}], offset: 0}, - } - const onChange = jest.fn() - await waitFor(() => { - render( - , - ) - }) - - const editor = editorRef.current! - expect(editor).toBeDefined() - - await waitFor(() => { - PortableTextEditor.select(editor, sel) - PortableTextEditor.focus(editor) - PortableTextEditor.insertBreak(editor) - expect(PortableTextEditor.getValue(editor)).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "1987f99da4a2", - "_type": "myTestBlockType", - "children": Array [ - Object { - "_key": "3693e789451c", - "_type": "span", - "marks": Array [], - "text": "1", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - Object { - "_key": "3", - "_type": "myTestBlockType", - "children": Array [ - Object { - "_key": "2", - "_type": "span", - "marks": Array [], - "text": "", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - Object { - "_key": "2f55670a03bb", - "_type": "myTestBlockType", - "children": Array [ - Object { - "_key": "9f5ed7dee7ab", - "_type": "span", - "marks": Array [ - "bab319ad3a9d", - ], - "text": "2", - }, - ], - "markDefs": Array [ - Object { - "_key": "bab319ad3a9d", - "_type": "link", - "href": "http://www.123.com", - }, - ], - "style": "normal", - }, - ] - `) - }) - }) - }) - describe('selection', () => { - it('should emit a new selection object when toggling marks, even though the value is the same', async () => { - const editorRef: RefObject = createRef() - const initialValue = [ - { - _key: '1987f99da4a2', - _type: 'myTestBlockType', - children: [ - { - _key: '3693e789451c', - _type: 'span', - marks: [], - text: '', - }, - ], - markDefs: [], - style: 'normal', - }, - ] - const onChange = jest.fn() - - await waitFor(() => { - render( - , - ) - }) - - const editor = editorRef.current! - expect(editor).toBeDefined() - - await waitFor(() => { - PortableTextEditor.focus(editor) - }) - const currentSelectionObject = PortableTextEditor.getSelection(editor) - - await waitFor(() => { - PortableTextEditor.toggleMark(editor, 'strong') - }) - const nextSelectionObject = PortableTextEditor.getSelection(editor) - expect(currentSelectionObject).toEqual(nextSelectionObject) - expect(currentSelectionObject === nextSelectionObject).toBe(false) - expect(onChange).toHaveBeenCalledWith({type: 'selection', selection: nextSelectionObject}) - }) - - it('should return active marks that cover the whole selection', async () => { - const editorRef: RefObject = createRef() - const initialValue = [ - { - _key: 'a', - _type: 'myTestBlockType', - children: [ - { - _key: 'a1', - _type: 'span', - marks: ['strong'], - text: '12', - }, - { - _key: '2', - _type: 'span', - marks: [], - text: '34', - }, - ], - markDefs: [{_key: 'strong', _type: 'strong'}], - style: 'normal', - }, - ] - const onChange = jest.fn() - await waitFor(() => { - render( - , - ) - }) - const editor = editorRef.current! - expect(editor).toBeDefined() - await waitFor(() => { - PortableTextEditor.focus(editor) - PortableTextEditor.select(editor, { - focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 0}, - anchor: {path: [{_key: 'a'}, 'children', {_key: '2'}], offset: 2}, - }) - expect(PortableTextEditor.isMarkActive(editor, 'strong')).toBe(false) - PortableTextEditor.toggleMark(editor, 'strong') - expect(PortableTextEditor.isMarkActive(editor, 'strong')).toBe(true) - }) - }) - - it('should return active annotation types that cover the whole selection', async () => { - const editorRef: RefObject = createRef() - const initialValue = [ - { - _key: 'a', - _type: 'myTestBlockType', - children: [ - { - _key: 'a1', - _type: 'span', - marks: ['bab319ad3a9d'], - text: '12', - }, - { - _key: '2', - _type: 'span', - marks: [], - text: '34', - }, - ], - markDefs: [ - { - _key: 'bab319ad3a9d', - _type: 'link', - href: 'http://www.123.com', - }, - ], - style: 'normal', - }, - ] - const onChange = jest.fn() - await waitFor(() => { - render( - , - ) - }) - const editor = editorRef.current! - expect(editor).toBeDefined() - await waitFor(() => { - PortableTextEditor.focus(editor) - PortableTextEditor.select(editor, { - focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 0}, - anchor: {path: [{_key: 'a'}, 'children', {_key: '2'}], offset: 2}, - }) - expect(PortableTextEditor.isAnnotationActive(editor, 'link')).toBe(false) - }) - }) - }) - - describe('removing annotations', () => { - it('removes the markDefs if the annotation is no longer in use', async () => { - const editorRef: RefObject = createRef() - const initialValue = [ - { - _key: '5fc57af23597', - _type: 'myTestBlockType', - children: [ - { - _key: 'be1c67c6971a', - _type: 'span', - marks: ['fde1fd54b544'], - text: 'This is a link', - }, - ], - markDefs: [ - { - _key: 'fde1fd54b544', - _type: 'link', - url: '1', - }, - ], - style: 'normal', - }, - ] - const onChange = jest.fn() - await waitFor(() => { - render( - , - ) - }) - - await waitFor(() => { - if (editorRef.current) { - PortableTextEditor.focus(editorRef.current) - PortableTextEditor.select(editorRef.current, { - focus: {path: [{_key: '5fc57af23597'}, 'children', {_key: 'be1c67c6971a'}], offset: 14}, - anchor: {path: [{_key: '5fc57af23597'}, 'children', {_key: 'be1c67c6971a'}], offset: 0}, - }) - // // eslint-disable-next-line max-nested-callbacks - const linkType = editorRef.current.schemaTypes.annotations.find((a) => a.name === 'link') - if (!linkType) { - throw new Error('No link type found') - } - PortableTextEditor.removeAnnotation(editorRef.current, linkType) - expect(PortableTextEditor.getValue(editorRef.current)).toEqual([ - { - _key: '5fc57af23597', - _type: 'myTestBlockType', - children: [ - { - _key: 'be1c67c6971a', - _type: 'span', - marks: [], - text: 'This is a link', - }, - ], - markDefs: [], - style: 'normal', - }, - ]) - } - }) - }) - it('preserves the markDefs if the annotation will continue in use', async () => { - const editorRef: RefObject = createRef() - const initialValue = [ - { - _key: '5fc57af23597', - _type: 'myTestBlockType', - children: [ - { - _key: 'be1c67c6971a', - _type: 'span', - marks: ['fde1fd54b544'], - text: 'This is a link', - }, - ], - markDefs: [ - { - _key: 'fde1fd54b544', - _type: 'link', - url: '1', - }, - ], - style: 'normal', - }, - ] - const onChange = jest.fn() - await waitFor(() => { - render( - , - ) - }) - - await waitFor(() => { - if (editorRef.current) { - PortableTextEditor.focus(editorRef.current) - PortableTextEditor.select(editorRef.current, { - focus: {path: [{_key: '5fc57af23597'}, 'children', {_key: 'be1c67c6971a'}], offset: 10}, - anchor: { - path: [{_key: '5fc57af23597'}, 'children', {_key: 'be1c67c6971a'}], - offset: 0, - }, - }) - // // eslint-disable-next-line max-nested-callbacks - const linkType = editorRef.current.schemaTypes.annotations.find((a) => a.name === 'link') - if (!linkType) { - throw new Error('No link type found') - } - PortableTextEditor.removeAnnotation(editorRef.current, linkType) - expect(PortableTextEditor.getValue(editorRef.current)).toEqual([ - { - _key: '5fc57af23597', - _type: 'myTestBlockType', - children: [ - { - _key: 'be1c67c6971a', - _type: 'span', - marks: [], - text: 'This is a ', - }, - { - _key: '1', - marks: ['fde1fd54b544'], - _type: 'span', - text: 'link', - }, - ], - markDefs: [ - { - _key: 'fde1fd54b544', - _type: 'link', - url: '1', - }, - ], - style: 'normal', - }, - ]) - } - }) - }) - it('removes the mark from the correct place', async () => { - const editorRef: RefObject = createRef() - const initialValue = [ - { - _key: '5fc57af23597', - _type: 'myTestBlockType', - children: [ - { - _key: 'be1c67c6971a', - _type: 'span', - marks: ['fde1fd54b544'], - text: 'This is a link', - }, - ], - markDefs: [ - { - _key: 'fde1fd54b544', - _type: 'link', - url: '1', - }, - ], - style: 'normal', - }, - ] - const onChange = jest.fn() - await waitFor(() => { - render( - , - ) - }) - - await waitFor(() => { - if (editorRef.current) { - PortableTextEditor.focus(editorRef.current) - // Selects `a link` from `This is a link`, so the mark should be kept in the first span. - PortableTextEditor.select(editorRef.current, { - focus: {path: [{_key: '5fc57af23597'}, 'children', {_key: 'be1c67c6971a'}], offset: 14}, - anchor: { - path: [{_key: '5fc57af23597'}, 'children', {_key: 'be1c67c6971a'}], - offset: 8, - }, - }) - - // // eslint-disable-next-line max-nested-callbacks - const linkType = editorRef.current.schemaTypes.annotations.find((a) => a.name === 'link') - if (!linkType) { - throw new Error('No link type found') - } - PortableTextEditor.removeAnnotation(editorRef.current, linkType) - expect(PortableTextEditor.getValue(editorRef.current)).toEqual([ - { - _key: '5fc57af23597', - _type: 'myTestBlockType', - children: [ - { - _key: 'be1c67c6971a', - _type: 'span', - marks: ['fde1fd54b544'], - text: 'This is ', - }, - { - _key: '1', - _type: 'span', - marks: [], - text: 'a link', - }, - ], - markDefs: [ - { - _key: 'fde1fd54b544', - _type: 'link', - url: '1', - }, - ], - style: 'normal', - }, - ]) - } - }) - }) - it('preserves other marks that apply to the spans', async () => { - const editorRef: RefObject = createRef() - const initialValue = [ - { - _key: '5fc57af23597', - _type: 'myTestBlockType', - children: [ - { - _key: 'be1c67c6971a', - _type: 'span', - marks: ['fde1fd54b544', '7b6d3d5de30c'], - text: 'This is a link', - }, - ], - markDefs: [ - { - _key: 'fde1fd54b544', - _type: 'link', - url: '1', - }, - { - _key: '7b6d3d5de30c', - _type: 'color', - color: 'blue', - }, - ], - style: 'normal', - }, - ] - const onChange = jest.fn() - await waitFor(() => { - render( - , - ) - }) - - await waitFor(() => { - if (editorRef.current) { - PortableTextEditor.focus(editorRef.current) - // Selects `a link` from `This is a link`, so the mark should be kept in the first span, color mark in both. - PortableTextEditor.select(editorRef.current, { - focus: {path: [{_key: '5fc57af23597'}, 'children', {_key: 'be1c67c6971a'}], offset: 14}, - anchor: { - path: [{_key: '5fc57af23597'}, 'children', {_key: 'be1c67c6971a'}], - offset: 8, - }, - }) - - // // eslint-disable-next-line max-nested-callbacks - const linkType = editorRef.current.schemaTypes.annotations.find((a) => a.name === 'link') - if (!linkType) { - throw new Error('No link type found') - } - PortableTextEditor.removeAnnotation(editorRef.current, linkType) - expect(PortableTextEditor.getValue(editorRef.current)).toEqual([ - { - _key: '5fc57af23597', - _type: 'myTestBlockType', - children: [ - { - _key: 'be1c67c6971a', - _type: 'span', - marks: ['fde1fd54b544', '7b6d3d5de30c'], // It has both marks, the link was only removed from the second span - text: 'This is ', - }, - { - _key: '1', - _type: 'span', - marks: ['7b6d3d5de30c'], - text: 'a link', - }, - ], - markDefs: [ - { - _key: 'fde1fd54b544', - _type: 'link', - url: '1', - }, - { - _key: '7b6d3d5de30c', - _type: 'color', - color: 'blue', - }, - ], - style: 'normal', - }, - ]) - - // removes the color from both - PortableTextEditor.select(editorRef.current, { - focus: {path: [{_key: '5fc57af23597'}, 'children', {_key: '1'}], offset: 6}, - anchor: { - path: [{_key: '5fc57af23597'}, 'children', {_key: 'be1c67c6971a'}], - offset: 0, - }, - }) - const colorType = editorRef.current.schemaTypes.annotations.find( - (a) => a.name === 'color', - ) - if (!colorType) { - throw new Error('No color type found') - } - - PortableTextEditor.removeAnnotation(editorRef.current, colorType) - - expect(PortableTextEditor.getValue(editorRef.current)).toEqual([ - { - _key: '5fc57af23597', - _type: 'myTestBlockType', - children: [ - { - _key: 'be1c67c6971a', - _type: 'span', - marks: ['fde1fd54b544'], // The color was removed from both - text: 'This is ', - }, - { - _key: '1', - _type: 'span', - marks: [], // The color was removed from both - text: 'a link', - }, - ], - markDefs: [ - { - _key: 'fde1fd54b544', - _type: 'link', - url: '1', - }, - ], - style: 'normal', - }, - ]) - } - }) - }) - }) - - describe('removing nodes', () => { - it("should insert an empty text block if it's removing the only block", async () => { - const editorRef: RefObject = createRef() - const initialValue = [ - { - _key: '5fc57af23597', - _type: 'someObject', - }, - ] - const onChange = jest.fn() - await waitFor(() => { - render( - , - ) - }) - - await waitFor(() => { - if (editorRef.current) { - PortableTextEditor.focus(editorRef.current) - - PortableTextEditor.delete( - editorRef.current, - { - focus: {path: [{_key: '5fc57af23597'}], offset: 0}, - anchor: {path: [{_key: '5fc57af23597'}], offset: 0}, - }, - {mode: 'blocks'}, - ) - - const value = PortableTextEditor.getValue(editorRef.current) - - expect(value).toEqual([ - { - _type: 'myTestBlockType', - _key: '3', - style: 'normal', - markDefs: [], - children: [ - { - _type: 'span', - _key: '2', - text: '', - marks: [], - }, - ], - }, - ]) - } - }) - }) - it('should not insert a new block if we have more blocks available', async () => { - const editorRef: RefObject = createRef() - const initialValue = [ - { - _key: '5fc57af23597', - _type: 'someObject', - }, - { - _type: 'myTestBlockType', - _key: 'existingBlock', - style: 'normal', - markDefs: [], - children: [ - { - _type: 'span', - _key: '2', - text: '', - marks: [], - }, - ], - }, - ] - const onChange = jest.fn() - await waitFor(() => { - render( - , - ) - }) - - await waitFor(() => { - if (editorRef.current) { - PortableTextEditor.focus(editorRef.current) - - PortableTextEditor.delete( - editorRef.current, - { - focus: {path: [{_key: '5fc57af23597'}], offset: 0}, - anchor: {path: [{_key: '5fc57af23597'}], offset: 0}, - }, - {mode: 'blocks'}, - ) - - const value = PortableTextEditor.getValue(editorRef.current) - expect(value).toEqual([ - { - _type: 'myTestBlockType', - _key: 'existingBlock', - style: 'normal', - markDefs: [], - children: [ - { - _type: 'span', - _key: '2', - text: '', - marks: [], - }, - ], - }, - ]) - } - }) - }) - }) -}) diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withPortableTextSelections.test.tsx b/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withPortableTextSelections.test.tsx deleted file mode 100644 index 17f2dd5d0a5..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withPortableTextSelections.test.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import {describe, expect, it, jest} from '@jest/globals' -import {render, waitFor} from '@testing-library/react' -import {createRef, type RefObject} from 'react' - -import {PortableTextEditorTester, schemaType} from '../../__tests__/PortableTextEditorTester' -import {PortableTextEditor} from '../../PortableTextEditor' - -const initialValue = [ - { - _key: 'a', - _type: 'myTestBlockType', - children: [ - { - _key: 'a1', - _type: 'span', - marks: [], - text: "It's a beautiful day on planet earth", - }, - ], - markDefs: [], - style: 'normal', - }, - { - _key: 'b', - _type: 'myTestBlockType', - children: [ - { - _key: 'b1', - _type: 'span', - marks: [], - text: 'The birds are singing', - }, - ], - markDefs: [], - style: 'normal', - }, -] - -describe('plugin:withPortableTextSelections', () => { - it('will report that a selection is made backward', async () => { - const editorRef: RefObject = createRef() - const onChange = jest.fn() - render( - , - ) - const initialSelection = { - anchor: {path: [{_key: 'b'}, 'children', {_key: 'b1'}], offset: 9}, - focus: {path: [{_key: 'a'}, 'children', {_key: 'a1'}], offset: 7}, - } - await waitFor(() => { - if (editorRef.current) { - PortableTextEditor.focus(editorRef.current) - PortableTextEditor.select(editorRef.current, initialSelection) - expect(PortableTextEditor.getSelection(editorRef.current)).toMatchInlineSnapshot(` - Object { - "anchor": Object { - "offset": 9, - "path": Array [ - Object { - "_key": "b", - }, - "children", - Object { - "_key": "b1", - }, - ], - }, - "backward": true, - "focus": Object { - "offset": 7, - "path": Array [ - Object { - "_key": "a", - }, - "children", - Object { - "_key": "a1", - }, - ], - }, - } - `) - } - }) - }) -}) diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withUndoRedo.test.tsx b/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withUndoRedo.test.tsx deleted file mode 100644 index 82df9255d87..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withUndoRedo.test.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import {describe, expect, it, jest} from '@jest/globals' -import {render, waitFor} from '@testing-library/react' -import {createRef, type RefObject} from 'react' - -import {PortableTextEditorTester, schemaType} from '../../__tests__/PortableTextEditorTester' -import {PortableTextEditor} from '../../PortableTextEditor' - -const initialValue = [ - { - _key: 'a', - _type: 'myTestBlockType', - children: [ - { - _key: 'a1', - _type: 'span', - marks: [], - text: 'Block A', - }, - ], - markDefs: [], - style: 'normal', - }, - { - _key: 'b', - _type: 'myTestBlockType', - children: [ - { - _key: 'b1', - _type: 'span', - marks: [], - text: 'Block B', - }, - ], - markDefs: [], - style: 'normal', - }, -] - -const initialSelection = { - focus: {path: [{_key: 'b'}, 'children', {_key: 'b1'}], offset: 7}, - anchor: {path: [{_key: 'b'}, 'children', {_key: 'b1'}], offset: 7}, -} - -describe('plugin:withUndoRedo', () => { - it('preserves the keys when undoing ', async () => { - const editorRef: RefObject = createRef() - const onChange = jest.fn() - render( - , - ) - await waitFor(() => { - if (editorRef.current) { - PortableTextEditor.focus(editorRef.current) - PortableTextEditor.select(editorRef.current, initialSelection) - PortableTextEditor.delete( - editorRef.current, - PortableTextEditor.getSelection(editorRef.current), - {mode: 'blocks'}, - ) - expect(PortableTextEditor.getValue(editorRef.current)).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "a", - "_type": "myTestBlockType", - "children": Array [ - Object { - "_key": "a1", - "_type": "span", - "marks": Array [], - "text": "Block A", - }, - ], - "markDefs": Array [], - "style": "normal", - }, - ] - `) - PortableTextEditor.undo(editorRef.current) - expect(PortableTextEditor.getValue(editorRef.current)).toEqual(initialValue) - } - }) - }) - it('preserves the keys when redoing ', async () => { - const editorRef: RefObject = createRef() - const onChange = jest.fn() - render( - , - ) - await waitFor(() => { - if (editorRef.current) { - PortableTextEditor.focus(editorRef.current) - PortableTextEditor.select(editorRef.current, initialSelection) - PortableTextEditor.insertBlock(editorRef.current, editorRef.current.schemaTypes.block, { - children: [{_key: 'c1', _type: 'span', marks: [], text: 'Block C'}], - }) - const producedKey = PortableTextEditor.getValue(editorRef.current)?.slice(-1)[0]?._key - PortableTextEditor.undo(editorRef.current) - PortableTextEditor.redo(editorRef.current) - expect(PortableTextEditor.getValue(editorRef.current)?.slice(-1)[0]?._key).toEqual( - producedKey, - ) - } - }) - }) -}) diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithEditableAPI.ts b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithEditableAPI.ts deleted file mode 100644 index 25c9368588b..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithEditableAPI.ts +++ /dev/null @@ -1,573 +0,0 @@ -import { - isPortableTextSpan, - type ObjectSchemaType, - type Path, - type PortableTextBlock, - type PortableTextChild, - type PortableTextObject, - type PortableTextTextBlock, - type SchemaType, -} from '@sanity/types' -import {Editor, Element as SlateElement, Node, Range, Text, Transforms} from 'slate' -import {ReactEditor} from 'slate-react' -import {type DOMNode} from 'slate-react/dist/utils/dom' - -import { - type EditableAPIDeleteOptions, - type EditorSelection, - type PortableTextMemberSchemaTypes, - type PortableTextSlateEditor, -} from '../../types/editor' -import {debugWithName} from '../../utils/debug' -import {toPortableTextRange, toSlateRange} from '../../utils/ranges' -import {fromSlateValue, isEqualToEmptyEditor, toSlateValue} from '../../utils/values' -import {KEY_TO_VALUE_ELEMENT, SLATE_TO_PORTABLE_TEXT_RANGE} from '../../utils/weakMaps' -import {type PortableTextEditor} from '../PortableTextEditor' - -const debug = debugWithName('API:editable') - -export function createWithEditableAPI( - portableTextEditor: PortableTextEditor, - types: PortableTextMemberSchemaTypes, - keyGenerator: () => string, -) { - return function withEditableAPI(editor: PortableTextSlateEditor): PortableTextSlateEditor { - portableTextEditor.setEditable({ - focus: (): void => { - ReactEditor.focus(editor) - }, - blur: (): void => { - ReactEditor.blur(editor) - }, - toggleMark: (mark: string): void => { - editor.pteToggleMark(mark) - }, - toggleList: (listStyle: string): void => { - editor.pteToggleListItem(listStyle) - }, - toggleBlockStyle: (blockStyle: string): void => { - editor.pteToggleBlockStyle(blockStyle) - }, - isMarkActive: (mark: string): boolean => { - // Try/catch this, as Slate may error because the selection is currently wrong - // TODO: catch only relevant error from Slate - try { - return editor.pteIsMarkActive(mark) - } catch (err) { - console.warn(err) - return false - } - }, - marks: (): string[] => { - return ( - { - ...(Editor.marks(editor) || {}), - }.marks || [] - ) - }, - undo: (): void => editor.undo(), - redo: (): void => editor.redo(), - select: (selection: EditorSelection): void => { - const slateSelection = toSlateRange(selection, editor) - if (slateSelection) { - Transforms.select(editor, slateSelection) - } else { - Transforms.deselect(editor) - } - editor.onChange() - }, - focusBlock: (): PortableTextBlock | undefined => { - if (editor.selection) { - const block = Node.descendant(editor, editor.selection.focus.path.slice(0, 1)) - if (block) { - return fromSlateValue([block], types.block.name, KEY_TO_VALUE_ELEMENT.get(editor))[0] - } - } - return undefined - }, - focusChild: (): PortableTextChild | undefined => { - if (editor.selection) { - const block = Node.descendant(editor, editor.selection.focus.path.slice(0, 1)) - if (block && editor.isTextBlock(block)) { - const ptBlock = fromSlateValue( - [block], - types.block.name, - KEY_TO_VALUE_ELEMENT.get(editor), - )[0] as PortableTextTextBlock - return ptBlock.children[editor.selection.focus.path[1]] - } - } - return undefined - }, - insertChild: (type: SchemaType, value?: {[prop: string]: any}): Path => { - if (!editor.selection) { - throw new Error('The editor has no selection') - } - const [focusBlock] = Array.from( - Editor.nodes(editor, { - at: editor.selection.focus.path.slice(0, 1), - match: (n) => n._type === types.block.name, - }), - )[0] || [undefined] - if (!focusBlock) { - throw new Error('No focused text block') - } - if ( - type.name !== types.span.name && - !types.inlineObjects.some((t) => t.name === type.name) - ) { - throw new Error('This type cannot be inserted as a child to a text block') - } - const block = toSlateValue( - [ - { - _key: keyGenerator(), - _type: types.block.name, - children: [ - { - _key: keyGenerator(), - _type: type.name, - ...(value ? value : {}), - }, - ], - }, - ], - portableTextEditor, - )[0] as unknown as SlateElement - const child = block.children[0] - const focusChildPath = editor.selection.focus.path.slice(0, 2) - const isSpanNode = child._type === types.span.name - const focusNode = Node.get(editor, focusChildPath) - - // If we are inserting a span, and currently have focus on an inline object, - // move the selection to the next span (guaranteed by normalizing rules) before inserting it. - if (isSpanNode && focusNode._type !== types.span.name) { - debug('Inserting span child next to inline object child, moving selection + 1') - editor.move({distance: 1, unit: 'character'}) - } - - Transforms.insertNodes(editor, child, { - select: true, - at: editor.selection, - }) - editor.onChange() - return ( - toPortableTextRange( - fromSlateValue(editor.children, types.block.name, KEY_TO_VALUE_ELEMENT.get(editor)), - editor.selection, - types, - )?.focus.path || [] - ) - }, - insertBlock: (type: SchemaType, value?: {[prop: string]: any}): Path => { - if (!editor.selection) { - throw new Error('The editor has no selection') - } - const block = toSlateValue( - [ - { - _key: keyGenerator(), - _type: type.name, - ...(value ? value : {}), - }, - ], - portableTextEditor, - )[0] as unknown as Node - const [focusBlock] = Array.from( - Editor.nodes(editor, { - at: editor.selection.focus.path.slice(0, 1), - match: (n) => n._type === types.block.name, - }), - )[0] || [undefined] - - const isEmptyTextBlock = focusBlock && isEqualToEmptyEditor([focusBlock], types) - - if (isEmptyTextBlock) { - // If the text block is empty, remove it before inserting the new block. - Transforms.removeNodes(editor, {at: editor.selection}) - } - - Editor.insertNode(editor, block) - editor.onChange() - return ( - toPortableTextRange( - fromSlateValue(editor.children, types.block.name, KEY_TO_VALUE_ELEMENT.get(editor)), - editor.selection, - types, - )?.focus.path || [] - ) - }, - hasBlockStyle: (style: string): boolean => { - try { - return editor.pteHasBlockStyle(style) - } catch (err) { - // This is fine. - // debug(err) - return false - } - }, - hasListStyle: (listStyle: string): boolean => { - try { - return editor.pteHasListStyle(listStyle) - } catch (err) { - // This is fine. - // debug(err) - return false - } - }, - isVoid: (element: PortableTextBlock | PortableTextChild) => { - return ![types.block.name, types.span.name].includes(element._type) - }, - findByPath: ( - path: Path, - ): [PortableTextBlock | PortableTextChild | undefined, Path | undefined] => { - const slatePath = toSlateRange( - {focus: {path, offset: 0}, anchor: {path, offset: 0}}, - editor, - ) - if (slatePath) { - const [block, blockPath] = Editor.node(editor, slatePath.focus.path.slice(0, 1)) - if (block && blockPath && typeof block._key === 'string') { - if (path.length === 1 && slatePath.focus.path.length === 1) { - return [fromSlateValue([block], types.block.name)[0], [{_key: block._key}]] - } - const ptBlock = fromSlateValue( - [block], - types.block.name, - KEY_TO_VALUE_ELEMENT.get(editor), - )[0] - if (editor.isTextBlock(ptBlock)) { - const ptChild = ptBlock.children[slatePath.focus.path[1]] - if (ptChild) { - return [ptChild, [{_key: block._key}, 'children', {_key: ptChild._key}]] - } - } - } - } - return [undefined, undefined] - }, - findDOMNode: (element: PortableTextBlock | PortableTextChild): DOMNode | undefined => { - let node: DOMNode | undefined - try { - const [item] = Array.from( - Editor.nodes(editor, { - at: [], - match: (n) => n._key === element._key, - }) || [], - )[0] || [undefined] - node = ReactEditor.toDOMNode(editor, item) - } catch (err) { - // Nothing - } - return node - }, - activeAnnotations: (): PortableTextObject[] => { - if (!editor.selection || editor.selection.focus.path.length < 2) { - return [] - } - try { - const activeAnnotations: PortableTextObject[] = [] - const spans = Editor.nodes(editor, { - at: editor.selection, - match: (node) => - Text.isText(node) && - node.marks !== undefined && - Array.isArray(node.marks) && - node.marks.length > 0, - }) - for (const [span, path] of spans) { - const [block] = Editor.node(editor, path, {depth: 1}) - if (editor.isTextBlock(block)) { - block.markDefs?.forEach((def) => { - if ( - Text.isText(span) && - span.marks && - Array.isArray(span.marks) && - span.marks.includes(def._key) - ) { - activeAnnotations.push(def) - } - }) - } - } - return activeAnnotations - } catch (err) { - return [] - } - }, - isAnnotationActive: (annotationType: PortableTextObject['_type']): boolean => { - if (!editor.selection || editor.selection.focus.path.length < 2) { - return false - } - - try { - const spans = [ - ...Editor.nodes(editor, { - at: editor.selection, - match: (node) => Text.isText(node), - }), - ] - - if ( - spans.some( - ([span]) => !isPortableTextSpan(span) || !span.marks || span.marks?.length === 0, - ) - ) - return false - - const selectionMarkDefs = spans.reduce((accMarkDefs, [, path]) => { - const [block] = Editor.node(editor, path, {depth: 1}) - if (editor.isTextBlock(block) && block.markDefs) { - return [...accMarkDefs, ...block.markDefs] - } - return accMarkDefs - }, [] as PortableTextObject[]) - - return spans.every(([span]) => { - if (!isPortableTextSpan(span)) return false - - const spanMarkDefs = span.marks?.map( - (markKey) => selectionMarkDefs.find((def) => def?._key === markKey)?._type, - ) - - return spanMarkDefs?.includes(annotationType) - }) - } catch (err) { - return false - } - }, - addAnnotation: ( - type: ObjectSchemaType, - value?: {[prop: string]: unknown}, - ): {spanPath: Path; markDefPath: Path} | undefined => { - const {selection: originalSelection} = editor - let returnValue: {spanPath: Path; markDefPath: Path} | undefined = undefined - if (originalSelection) { - const [block] = Editor.node(editor, originalSelection.focus, {depth: 1}) - if (!editor.isTextBlock(block)) { - return undefined - } - if (Range.isCollapsed(originalSelection)) { - editor.pteExpandToWord() - editor.onChange() - } - const [textNode] = Editor.node(editor, originalSelection.focus, {depth: 2}) - - // If we still have a selection, add the annotation to the selected text - if (editor.selection) { - Editor.withoutNormalizing(editor, () => { - // Add markDefs to the block - const annotationKey = keyGenerator() - Transforms.setNodes( - editor, - { - markDefs: [ - ...(block.markDefs || []), - {_type: type.name, _key: annotationKey, ...value} as PortableTextObject, - ], - }, - {at: originalSelection.focus}, - ) - editor.onChange() - - // Split if needed - Transforms.setNodes(editor, {}, {match: Text.isText, split: true}) - editor.onChange() - - // Add marks to the span node - if (editor.selection && Text.isText(textNode)) { - Transforms.setNodes( - editor, - { - marks: [...((textNode.marks || []) as string[]), annotationKey], - }, - { - at: editor.selection, - match: (n) => n._type === types.span.name, - }, - ) - } - editor.onChange() - if (editor.selection) { - // Insert an empty string to continue writing non-annotated text - Transforms.insertNodes( - editor, - [{_type: 'span', text: '', marks: [], _key: keyGenerator()}], - { - at: Range.end(editor.selection), - }, - ) - } - const newPortableTextEditorSelection = toPortableTextRange( - fromSlateValue(editor.children, types.block.name, KEY_TO_VALUE_ELEMENT.get(editor)), - editor.selection, - types, - ) - if (newPortableTextEditorSelection) { - returnValue = { - spanPath: newPortableTextEditorSelection.focus.path, - markDefPath: [{_key: block._key}, 'markDefs', {_key: annotationKey}], - } - } - }) - Editor.normalize(editor) - editor.onChange() - } - } - return returnValue - }, - delete: (selection: EditorSelection, options?: EditableAPIDeleteOptions): void => { - if (selection) { - const range = toSlateRange(selection, editor) - const hasRange = range && range.anchor.path.length > 0 && range.focus.path.length > 0 - if (!hasRange) { - throw new Error('Invalid range') - } - if (range) { - if (!options?.mode || options?.mode === 'selected') { - debug(`Deleting content in selection`) - Transforms.delete(editor, { - at: range, - hanging: true, - voids: true, - }) - editor.onChange() - return - } - if (options?.mode === 'blocks') { - debug(`Deleting blocks touched by selection`) - Transforms.removeNodes(editor, { - at: range, - voids: true, - match: (node) => { - return ( - editor.isTextBlock(node) || - (!editor.isTextBlock(node) && SlateElement.isElement(node)) - ) - }, - }) - } - if (options?.mode === 'children') { - debug(`Deleting children touched by selection`) - Transforms.removeNodes(editor, { - at: range, - voids: true, - match: (node) => { - return ( - node._type === types.span.name || // Text children - (!editor.isTextBlock(node) && SlateElement.isElement(node)) // inline blocks - ) - }, - }) - } - // If the editor was emptied, insert a placeholder block - // directly into the editor's children. We don't want to do this - // through a Transform (because that would trigger a change event - // that would insert the placeholder into the actual value - // which should remain empty) - if (editor.children.length === 0) { - editor.children = [editor.pteCreateEmptyBlock()] - } - editor.onChange() - } - } - }, - removeAnnotation: (type: ObjectSchemaType): void => { - let {selection} = editor - debug('Removing annotation', type) - if (selection) { - // Select the whole annotation if collapsed - if (Range.isCollapsed(selection)) { - const [node, nodePath] = Editor.node(editor, selection, {depth: 2}) - if (Text.isText(node) && node.marks && typeof node.text === 'string') { - Transforms.select(editor, nodePath) - selection = editor.selection - } - } - // Do this without normalization or span references will be unstable! - Editor.withoutNormalizing(editor, () => { - if (selection && Range.isExpanded(selection)) { - selection = editor.selection - if (!selection) { - return - } - // Find the selected block, to identify the annotation to remove - const blocks = [ - ...Editor.nodes(editor, { - at: selection, - match: (node) => { - return ( - editor.isTextBlock(node) && - Array.isArray(node.markDefs) && - node.markDefs.some((def) => def._type === type.name) - ) - }, - }), - ] - const removedMarks: string[] = [] - - // Removes the marks from the text nodes - blocks.forEach(([block]) => { - if (editor.isTextBlock(block) && Array.isArray(block.markDefs)) { - const marksToRemove = block.markDefs.filter((def) => def._type === type.name) - marksToRemove.forEach((def) => { - if (!removedMarks.includes(def._key)) removedMarks.push(def._key) - Editor.removeMark(editor, def._key) - }) - } - }) - } - }) - Editor.normalize(editor) - editor.onChange() - } - }, - getSelection: (): EditorSelection | null => { - let ptRange: EditorSelection = null - if (editor.selection) { - const existing = SLATE_TO_PORTABLE_TEXT_RANGE.get(editor.selection) - if (existing) { - return existing - } - ptRange = toPortableTextRange( - fromSlateValue(editor.children, types.block.name, KEY_TO_VALUE_ELEMENT.get(editor)), - editor.selection, - types, - ) - SLATE_TO_PORTABLE_TEXT_RANGE.set(editor.selection, ptRange) - } - return ptRange - }, - getValue: () => { - return fromSlateValue(editor.children, types.block.name, KEY_TO_VALUE_ELEMENT.get(editor)) - }, - isCollapsedSelection: () => { - return !!editor.selection && Range.isCollapsed(editor.selection) - }, - isExpandedSelection: () => { - return !!editor.selection && Range.isExpanded(editor.selection) - }, - insertBreak: () => { - editor.insertBreak() - editor.onChange() - }, - getFragment: () => { - return fromSlateValue(editor.getFragment(), types.block.name) - }, - isSelectionsOverlapping: (selectionA: EditorSelection, selectionB: EditorSelection) => { - // Convert the selections to Slate ranges - const rangeA = toSlateRange(selectionA, editor) - const rangeB = toSlateRange(selectionB, editor) - - // Make sure the ranges are valid - const isValidRanges = Range.isRange(rangeA) && Range.isRange(rangeB) - - // Check if the ranges are overlapping - const isOverlapping = isValidRanges && Range.includes(rangeA, rangeB) - - return isOverlapping - }, - }) - return editor - } -} diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithHotKeys.ts b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithHotKeys.ts deleted file mode 100644 index 1a99e551cb7..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithHotKeys.ts +++ /dev/null @@ -1,304 +0,0 @@ -/* eslint-disable max-statements */ -/* eslint-disable complexity */ -import {isPortableTextSpan, isPortableTextTextBlock} from '@sanity/types' -import {isHotkey} from 'is-hotkey-esm' -import {type KeyboardEvent} from 'react' -import {Editor, Node, Path, Range, Transforms} from 'slate' -import {type ReactEditor} from 'slate-react' - -import {type PortableTextMemberSchemaTypes, type PortableTextSlateEditor} from '../../types/editor' -import {type HotkeyOptions} from '../../types/options' -import {type SlateTextBlock, type VoidElement} from '../../types/slate' -import {debugWithName} from '../../utils/debug' -import {type PortableTextEditor} from '../PortableTextEditor' - -const debug = debugWithName('plugin:withHotKeys') - -const DEFAULT_HOTKEYS: HotkeyOptions = { - marks: { - 'mod+b': 'strong', - 'mod+i': 'em', - 'mod+u': 'underline', - "mod+'": 'code', - }, - custom: {}, -} - -/** - * This plugin takes care of all hotkeys in the editor - * - */ -export function createWithHotkeys( - types: PortableTextMemberSchemaTypes, - portableTextEditor: PortableTextEditor, - hotkeysFromOptions?: HotkeyOptions, -): (editor: PortableTextSlateEditor & ReactEditor) => any { - const reservedHotkeys = ['enter', 'tab', 'shift', 'delete', 'end'] - const activeHotkeys = hotkeysFromOptions || DEFAULT_HOTKEYS // TODO: Merge where possible? A union? - return function withHotKeys(editor: PortableTextSlateEditor & ReactEditor) { - editor.pteWithHotKeys = (event: KeyboardEvent): void => { - // Wire up custom marks hotkeys - Object.keys(activeHotkeys).forEach((cat) => { - if (cat === 'marks') { - // eslint-disable-next-line guard-for-in - for (const hotkey in activeHotkeys[cat]) { - if (reservedHotkeys.includes(hotkey)) { - throw new Error(`The hotkey ${hotkey} is reserved!`) - } - if (isHotkey(hotkey, event.nativeEvent)) { - event.preventDefault() - const possibleMark = activeHotkeys[cat] - if (possibleMark) { - const mark = possibleMark[hotkey] - debug(`HotKey ${hotkey} to toggle ${mark}`) - editor.pteToggleMark(mark) - } - } - } - } - if (cat === 'custom') { - // eslint-disable-next-line guard-for-in - for (const hotkey in activeHotkeys[cat]) { - if (reservedHotkeys.includes(hotkey)) { - throw new Error(`The hotkey ${hotkey} is reserved!`) - } - if (isHotkey(hotkey, event.nativeEvent)) { - const possibleCommand = activeHotkeys[cat] - if (possibleCommand) { - const command = possibleCommand[hotkey] - command(event, portableTextEditor) - } - } - } - } - }) - - const isEnter = isHotkey('enter', event.nativeEvent) - const isTab = isHotkey('tab', event.nativeEvent) - const isShiftEnter = isHotkey('shift+enter', event.nativeEvent) - const isShiftTab = isHotkey('shift+tab', event.nativeEvent) - const isBackspace = isHotkey('backspace', event.nativeEvent) - const isDelete = isHotkey('delete', event.nativeEvent) - const isArrowDown = isHotkey('down', event.nativeEvent) - const isArrowUp = isHotkey('up', event.nativeEvent) - - // Check if the user is in a void block, in that case, add an empty text block below if there is no next block - if (isArrowDown && editor.selection) { - const focusBlock = Node.descendant(editor, editor.selection.focus.path.slice(0, 1)) as - | SlateTextBlock - | VoidElement - - if (focusBlock && Editor.isVoid(editor, focusBlock)) { - const nextPath = Path.next(editor.selection.focus.path.slice(0, 1)) - const nextBlock = Node.has(editor, nextPath) - if (!nextBlock) { - Transforms.insertNodes(editor, editor.pteCreateEmptyBlock(), {at: nextPath}) - editor.onChange() - return - } - } - } - if (isArrowUp && editor.selection) { - const isFirstBlock = editor.selection.focus.path[0] === 0 - const focusBlock = Node.descendant(editor, editor.selection.focus.path.slice(0, 1)) as - | SlateTextBlock - | VoidElement - - if (isFirstBlock && focusBlock && Editor.isVoid(editor, focusBlock)) { - Transforms.insertNodes(editor, editor.pteCreateEmptyBlock(), {at: [0]}) - Transforms.select(editor, {path: [0, 0], offset: 0}) - editor.onChange() - return - } - } - if ( - isBackspace && - editor.selection && - editor.selection.focus.path[0] === 0 && - Range.isCollapsed(editor.selection) - ) { - // If the block is text and we have a next block below, remove the current block - const focusBlock = Node.descendant(editor, editor.selection.focus.path.slice(0, 1)) as - | SlateTextBlock - | VoidElement - const nextPath = Path.next(editor.selection.focus.path.slice(0, 1)) - const nextBlock = Node.has(editor, nextPath) - const isTextBlock = isPortableTextTextBlock(focusBlock) - const isEmptyFocusBlock = - isTextBlock && focusBlock.children.length === 1 && focusBlock.children?.[0]?.text === '' - - if (nextBlock && isTextBlock && isEmptyFocusBlock) { - // Remove current block - event.preventDefault() - event.stopPropagation() - Transforms.removeNodes(editor, {match: (n) => n === focusBlock}) - editor.onChange() - return - } - } - // Disallow deleting void blocks by backspace from another line. - // Otherwise it's so easy to delete the void block above when trying to delete text on - // the line below or above - if ( - isBackspace && - editor.selection && - editor.selection.focus.path[0] > 0 && - Range.isCollapsed(editor.selection) - ) { - const prevPath = Path.previous(editor.selection.focus.path.slice(0, 1)) - const prevBlock = Node.descendant(editor, prevPath) as SlateTextBlock | VoidElement - const focusBlock = Node.descendant(editor, editor.selection.focus.path.slice(0, 1)) - if ( - prevBlock && - focusBlock && - Editor.isVoid(editor, prevBlock) && - editor.selection.focus.offset === 0 - ) { - debug('Preventing deleting void block above') - event.preventDefault() - event.stopPropagation() - - const isTextBlock = isPortableTextTextBlock(focusBlock) - const isEmptyFocusBlock = - isTextBlock && focusBlock.children.length === 1 && focusBlock.children?.[0]?.text === '' - - // If this is a not an text block or it is empty, simply remove it - if (!isTextBlock || isEmptyFocusBlock) { - Transforms.removeNodes(editor, {match: (n) => n === focusBlock}) - Transforms.select(editor, prevPath) - - editor.onChange() - return - } - - // If the focused block is a text node but it isn't empty, focus on the previous block - if (isTextBlock && !isEmptyFocusBlock) { - Transforms.select(editor, prevPath) - - editor.onChange() - return - } - - return - } - } - if ( - isDelete && - editor.selection && - editor.selection.focus.offset === 0 && - Range.isCollapsed(editor.selection) && - editor.children[editor.selection.focus.path[0] + 1] - ) { - const nextBlock = Node.descendant( - editor, - Path.next(editor.selection.focus.path.slice(0, 1)), - ) as SlateTextBlock | VoidElement - const focusBlockPath = editor.selection.focus.path.slice(0, 1) - const focusBlock = Node.descendant(editor, focusBlockPath) as SlateTextBlock | VoidElement - - if ( - nextBlock && - focusBlock && - !Editor.isVoid(editor, focusBlock) && - Editor.isVoid(editor, nextBlock) - ) { - debug('Preventing deleting void block below') - event.preventDefault() - event.stopPropagation() - Transforms.removeNodes(editor, {match: (n) => n === focusBlock}) - Transforms.select(editor, focusBlockPath) - editor.onChange() - return - } - } - - // Tab for lists - // Only steal tab when we are on a plain text span or we are at the start of the line (fallback if the whole block is annotated or contains a single inline object) - // Otherwise tab is reserved for accessability for buttons etc. - if ((isTab || isShiftTab) && editor.selection) { - const [focusChild] = Editor.node(editor, editor.selection.focus, {depth: 2}) - const [focusBlock] = isPortableTextSpan(focusChild) - ? Editor.node(editor, editor.selection.focus, {depth: 1}) - : [] - const hasAnnotationFocus = - focusChild && - isPortableTextTextBlock(focusBlock) && - isPortableTextSpan(focusChild) && - (focusChild.marks || ([] as string[])).filter((m) => - (focusBlock.markDefs || []).map((def) => def._key).includes(m), - ).length > 0 - const [start] = Range.edges(editor.selection) - const atStartOfNode = Editor.isStart(editor, start, start.path) - - if ( - focusChild && - isPortableTextSpan(focusChild) && - (!hasAnnotationFocus || atStartOfNode) && - editor.pteIncrementBlockLevels(isShiftTab) - ) { - event.preventDefault() - } - } - - // Deal with enter key combos - if (isEnter && !isShiftEnter && editor.selection) { - const focusBlockPath = editor.selection.focus.path.slice(0, 1) - const focusBlock = Node.descendant(editor, focusBlockPath) as SlateTextBlock | VoidElement - - // List item enter key - if (editor.isListBlock(focusBlock)) { - if (editor.pteEndList()) { - event.preventDefault() - } - return - } - - // Enter from another style than the first (default one) - if ( - editor.isTextBlock(focusBlock) && - focusBlock.style && - focusBlock.style !== types.styles[0].value - ) { - const [, end] = Range.edges(editor.selection) - const endAtEndOfNode = Editor.isEnd(editor, end, end.path) - if (endAtEndOfNode) { - Editor.insertNode(editor, editor.pteCreateEmptyBlock()) - event.preventDefault() - editor.onChange() - return - } - } - // Block object enter key - if (focusBlock && Editor.isVoid(editor, focusBlock)) { - Editor.insertNode(editor, editor.pteCreateEmptyBlock()) - event.preventDefault() - editor.onChange() - return - } - // Default enter key behavior - event.preventDefault() - editor.insertBreak() - editor.onChange() - } - - // Soft line breaks - if (isShiftEnter) { - event.preventDefault() - editor.insertText('\n') - return - } - - // Undo/redo - if (isHotkey('mod+z', event.nativeEvent)) { - event.preventDefault() - editor.undo() - return - } - if (isHotkey('mod+y', event.nativeEvent) || isHotkey('mod+shift+z', event.nativeEvent)) { - event.preventDefault() - editor.redo() - } - } - return editor - } -} diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithInsertBreak.ts b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithInsertBreak.ts deleted file mode 100644 index 37b0a64f032..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithInsertBreak.ts +++ /dev/null @@ -1,45 +0,0 @@ -import {Editor, Node, Path, Range, Transforms} from 'slate' - -import {type PortableTextMemberSchemaTypes, type PortableTextSlateEditor} from '../../types/editor' -import {type SlateTextBlock, type VoidElement} from '../../types/slate' -import {isEqualToEmptyEditor} from '../../utils/values' - -/** - * Changes default behavior of insertBreak to insert a new block instead of splitting current when the cursor is at the - * start of the block. - */ -export function createWithInsertBreak( - types: PortableTextMemberSchemaTypes, -): (editor: PortableTextSlateEditor) => PortableTextSlateEditor { - return function withInsertBreak(editor: PortableTextSlateEditor): PortableTextSlateEditor { - const {insertBreak} = editor - - editor.insertBreak = () => { - if (editor.selection) { - const focusBlockPath = editor.selection.focus.path.slice(0, 1) - const focusBlock = Node.descendant(editor, focusBlockPath) as SlateTextBlock | VoidElement - - if (editor.isTextBlock(focusBlock)) { - // Enter from another style than the first (default one) - const [, end] = Range.edges(editor.selection) - // If it's at the start of block, we want to preserve the current block key and insert a new one in the current position instead of splitting the node. - const isEndAtStartOfNode = Editor.isStart(editor, end, end.path) - const isEmptyTextBlock = focusBlock && isEqualToEmptyEditor([focusBlock], types) - if (isEndAtStartOfNode && !isEmptyTextBlock) { - Editor.insertNode(editor, editor.pteCreateEmptyBlock()) - const [nextBlockPath] = Path.next(focusBlockPath) - Transforms.select(editor, { - anchor: {path: [nextBlockPath, 0], offset: 0}, - focus: {path: [nextBlockPath, 0], offset: 0}, - }) - - editor.onChange() - return - } - } - } - insertBreak() - } - return editor - } -} diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithInsertData.ts b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithInsertData.ts deleted file mode 100644 index 023efb9f14e..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithInsertData.ts +++ /dev/null @@ -1,359 +0,0 @@ -import {htmlToBlocks, normalizeBlock} from '@sanity/block-tools' -import {type PortableTextBlock, type PortableTextChild} from '@sanity/types' -import {isEqual, uniq} from 'lodash' -import {type Descendant, Editor, type Node, Range, Transforms} from 'slate' -import {ReactEditor} from 'slate-react' - -import { - type EditorChanges, - type PortableTextMemberSchemaTypes, - type PortableTextSlateEditor, -} from '../../types/editor' -import {debugWithName} from '../../utils/debug' -import {validateValue} from '../../utils/validateValue' -import {fromSlateValue, isEqualToEmptyEditor, toSlateValue} from '../../utils/values' - -const debug = debugWithName('plugin:withInsertData') - -/** - * This plugin handles copy/paste in the editor - * - */ -export function createWithInsertData( - change$: EditorChanges, - schemaTypes: PortableTextMemberSchemaTypes, - keyGenerator: () => string, -) { - return function withInsertData(editor: PortableTextSlateEditor): PortableTextSlateEditor { - const blockTypeName = schemaTypes.block.name - const spanTypeName = schemaTypes.span.name - const whitespaceOnPasteMode = schemaTypes.block.options.unstable_whitespaceOnPasteMode - - const toPlainText = (blocks: PortableTextBlock[]) => { - return blocks - .map((block) => { - if (editor.isTextBlock(block)) { - return block.children - .map((child: PortableTextChild) => { - if (child._type === spanTypeName) { - return child.text - } - return `[${ - schemaTypes.inlineObjects.find((t) => t.name === child._type)?.title || 'Object' - }]` - }) - .join('') - } - return `[${ - schemaTypes.blockObjects.find((t) => t.name === block._type)?.title || 'Object' - }]` - }) - .join('\n\n') - } - - editor.setFragmentData = (data: DataTransfer, originEvent) => { - const {selection} = editor - - if (!selection) { - return - } - - const [start, end] = Range.edges(selection) - const startVoid = Editor.void(editor, {at: start.path}) - const endVoid = Editor.void(editor, {at: end.path}) - - if (Range.isCollapsed(selection) && !startVoid) { - return - } - - // Create a fake selection so that we can add a Base64-encoded copy of the - // fragment to the HTML, to decode on future pastes. - const domRange = ReactEditor.toDOMRange(editor, selection) - let contents = domRange.cloneContents() - // COMPAT: If the end node is a void node, we need to move the end of the - // range from the void node's spacer span, to the end of the void node's - // content, since the spacer is before void's content in the DOM. - if (endVoid) { - const [voidNode] = endVoid - const r = domRange.cloneRange() - const domNode = ReactEditor.toDOMNode(editor, voidNode) - r.setEndAfter(domNode) - contents = r.cloneContents() - } - // Remove any zero-width space spans from the cloned DOM so that they don't - // show up elsewhere when pasted. - Array.from(contents.querySelectorAll('[data-slate-zero-width]')).forEach((zw) => { - const isNewline = zw.getAttribute('data-slate-zero-width') === 'n' - zw.textContent = isNewline ? '\n' : '' - }) - // Clean up the clipboard HTML for editor spesific attributes - Array.from(contents.querySelectorAll('*')).forEach((elm) => { - elm.removeAttribute('contentEditable') - elm.removeAttribute('data-slate-inline') - elm.removeAttribute('data-slate-leaf') - elm.removeAttribute('data-slate-node') - elm.removeAttribute('data-slate-spacer') - elm.removeAttribute('data-slate-string') - elm.removeAttribute('data-slate-zero-width') - elm.removeAttribute('draggable') - for (const key in elm.attributes) { - if (elm.hasAttribute(key)) { - elm.removeAttribute(key) - } - } - }) - const div = contents.ownerDocument.createElement('div') - div.appendChild(contents) - div.setAttribute('hidden', 'true') - contents.ownerDocument.body.appendChild(div) - const asHTML = div.innerHTML - contents.ownerDocument.body.removeChild(div) - const fragment = editor.getFragment() - const portableText = fromSlateValue(fragment, blockTypeName) - - const asJSON = JSON.stringify(portableText) - const asPlainText = toPlainText(portableText) - data.clearData() - data.setData('text/plain', asPlainText) - data.setData('text/html', asHTML) - data.setData('application/json', asJSON) - data.setData('application/x-portable-text', asJSON) - debug('text', asPlainText) - data.setData('application/x-portable-text-event-origin', originEvent || 'external') - debug('Set fragment data', asJSON, asHTML) - } - - editor.insertPortableTextData = (data: DataTransfer): boolean => { - if (!editor.selection) { - return false - } - const pText = data.getData('application/x-portable-text') - const origin = data.getData('application/x-portable-text-event-origin') - debug(`Inserting portable text from ${origin} event`, pText) - if (pText) { - const parsed = JSON.parse(pText) as PortableTextBlock[] - if (Array.isArray(parsed) && parsed.length > 0) { - const slateValue = _regenerateKeys( - editor, - toSlateValue(parsed, {schemaTypes}), - keyGenerator, - spanTypeName, - ) - // Validate the result - const validation = validateValue(parsed, schemaTypes, keyGenerator) - // Bail out if it's not valid - if (!validation.valid && !validation.resolution?.autoResolve) { - const errorDescription = `${validation.resolution?.description}` - change$.next({ - type: 'error', - level: 'warning', - name: 'pasteError', - description: errorDescription, - data: validation, - }) - debug('Invalid insert result', validation) - return false - } - _insertFragment(editor, slateValue, schemaTypes) - return true - } - } - return false - } - - editor.insertTextOrHTMLData = (data: DataTransfer): boolean => { - if (!editor.selection) { - debug('No selection, not inserting') - return false - } - change$.next({type: 'loading', isLoading: true}) // This could potentially take some time - const html = data.getData('text/html') - const text = data.getData('text/plain') - - if (html || text) { - debug('Inserting data', data) - let portableText: PortableTextBlock[] - let fragment: Node[] - let insertedType - - if (html) { - portableText = htmlToBlocks(html, schemaTypes.portableText, { - unstable_whitespaceOnPasteMode: whitespaceOnPasteMode, - }).map((block) => normalizeBlock(block, {blockTypeName})) as PortableTextBlock[] - fragment = toSlateValue(portableText, {schemaTypes}) - insertedType = 'HTML' - - if (portableText.length === 0) { - return false - } - } else { - // plain text - const blocks = escapeHtml(text) - .split(/\n{2,}/) - .map((line) => - line ? `

    ${line.replace(/(?:\r\n|\r|\n)/g, '
    ')}

    ` : '

    ', - ) - .join('') - const textToHtml = `${blocks}` - portableText = htmlToBlocks(textToHtml, schemaTypes.portableText).map((block) => - normalizeBlock(block, {blockTypeName}), - ) as PortableTextBlock[] - fragment = toSlateValue(portableText, { - schemaTypes, - }) - insertedType = 'text' - } - - // Validate the result - const validation = validateValue(portableText, schemaTypes, keyGenerator) - - // Bail out if it's not valid - if (!validation.valid) { - const errorDescription = `Could not validate the resulting portable text to insert.\n${validation.resolution?.description}\nTry to insert as plain text (shift-paste) instead.` - change$.next({ - type: 'error', - level: 'warning', - name: 'pasteError', - description: errorDescription, - data: validation, - }) - debug('Invalid insert result', validation) - return false - } - debug(`Inserting ${insertedType} fragment at ${JSON.stringify(editor.selection)}`) - _insertFragment(editor, fragment, schemaTypes) - change$.next({type: 'loading', isLoading: false}) - return true - } - change$.next({type: 'loading', isLoading: false}) - return false - } - - editor.insertData = (data: DataTransfer) => { - if (!editor.insertPortableTextData(data)) { - editor.insertTextOrHTMLData(data) - } - } - - editor.insertFragmentData = (data: DataTransfer): boolean => { - const fragment = data.getData('application/x-portable-text') - if (fragment) { - const parsed = JSON.parse(fragment) - editor.insertFragment(parsed) - return true - } - return false - } - - return editor - } -} - -const entityMap: Record = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - '/': '/', - '`': '`', - '=': '=', -} -function escapeHtml(str: string) { - return String(str).replace(/[&<>"'`=/]/g, (s: string) => entityMap[s]) -} - -/** - * Shared helper function to regenerate the keys on a fragment. - * - * @internal - */ -function _regenerateKeys( - editor: PortableTextSlateEditor, - fragment: Descendant[], - keyGenerator: () => string, - spanTypeName: string, -): Descendant[] { - return fragment.map((node) => { - const newNode: Descendant = {...node} - // Ensure the copy has new keys - if (editor.isTextBlock(newNode)) { - newNode.markDefs = (newNode.markDefs || []).map((def) => { - const oldKey = def._key - const newKey = keyGenerator() - newNode.children = newNode.children.map((child) => - child._type === spanTypeName && editor.isTextSpan(child) - ? { - ...child, - marks: - child.marks && child.marks.includes(oldKey) - ? // eslint-disable-next-line max-nested-callbacks - [...child.marks].filter((mark) => mark !== oldKey).concat(newKey) - : child.marks, - } - : child, - ) - return {...def, _key: newKey} - }) - } - const nodeWithNewKeys = {...newNode, _key: keyGenerator()} - if (editor.isTextBlock(nodeWithNewKeys)) { - nodeWithNewKeys.children = nodeWithNewKeys.children.map((child) => ({ - ...child, - _key: keyGenerator(), - })) - } - return nodeWithNewKeys as Descendant - }) -} - -/** - * Shared helper function to insert the final fragment into the editor - * - * @internal - */ -function _insertFragment( - editor: PortableTextSlateEditor, - fragment: Descendant[], - schemaTypes: PortableTextMemberSchemaTypes, -) { - editor.withoutNormalizing(() => { - if (!editor.selection) { - return - } - // Ensure that markDefs for any annotations inside this fragment are copied over to the focused text block. - const [focusBlock, focusPath] = Editor.node(editor, editor.selection, {depth: 1}) - if (editor.isTextBlock(focusBlock) && editor.isTextBlock(fragment[0])) { - const {markDefs} = focusBlock - debug('Mixing markDefs of focusBlock and fragments[0] block', markDefs, fragment[0].markDefs) - if (!isEqual(markDefs, fragment[0].markDefs)) { - Transforms.setNodes( - editor, - { - markDefs: uniq([...(fragment[0].markDefs || []), ...(markDefs || [])]), - }, - {at: focusPath, mode: 'lowest', voids: false}, - ) - } - } - - const isPasteToEmptyEditor = isEqualToEmptyEditor(editor.children, schemaTypes) - - if (isPasteToEmptyEditor) { - // Special case for pasting directly into an empty editor (a placeholder block). - // When pasting content starting with multiple empty blocks, - // `editor.insertFragment` can potentially duplicate the keys of - // the placeholder block because of operations that happen - // inside `editor.insertFragment` (involves an `insert_node` operation). - // However by splitting the placeholder block first in this situation we are good. - Transforms.splitNodes(editor, {at: [0, 0]}) - editor.insertFragment(fragment) - Transforms.removeNodes(editor, {at: [0]}) - } else { - // All other inserts - editor.insertFragment(fragment) - } - }) - - editor.onChange() -} diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithMaxBlocks.ts b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithMaxBlocks.ts deleted file mode 100644 index f1fc204cfa0..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithMaxBlocks.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {type PortableTextSlateEditor} from '../../types/editor' - -/** - * This plugin makes sure that the PTE maxBlocks prop is respected - * - */ -export function createWithMaxBlocks(maxBlocks: number) { - return function withMaxBlocks(editor: PortableTextSlateEditor): PortableTextSlateEditor { - const {apply} = editor - editor.apply = (operation) => { - const rows = maxBlocks - if (rows > 0 && editor.children.length >= rows) { - if ( - (operation.type === 'insert_node' || operation.type === 'split_node') && - operation.path.length === 1 - ) { - return - } - } - apply(operation) - } - return editor - } -} diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithObjectKeys.ts b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithObjectKeys.ts deleted file mode 100644 index 5e6d2f5ccde..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithObjectKeys.ts +++ /dev/null @@ -1,63 +0,0 @@ -import {Editor, Element, Node, Transforms} from 'slate' - -import {type PortableTextMemberSchemaTypes, type PortableTextSlateEditor} from '../../types/editor' -import {isPreservingKeys, PRESERVE_KEYS} from '../../utils/withPreserveKeys' - -/** - * This plugin makes sure that every new node in the editor get a new _key prop when created - * - */ -export function createWithObjectKeys( - schemaTypes: PortableTextMemberSchemaTypes, - keyGenerator: () => string, -) { - return function withKeys(editor: PortableTextSlateEditor): PortableTextSlateEditor { - PRESERVE_KEYS.set(editor, false) - const {apply, normalizeNode} = editor - - // The apply function can be called with a scope (withPreserveKeys) that will - // preserve keys for the produced nodes if they have a _key property set already. - // The default behavior is to always generate a new key here. - // For example, when undoing and redoing we want to retain the keys, but - // when we create a new bold span by splitting a non-bold-span we want the produced node to get a new key. - editor.apply = (operation) => { - if (operation.type === 'split_node') { - const withNewKey = !isPreservingKeys(editor) || !('_key' in operation.properties) - operation.properties = { - ...operation.properties, - ...(withNewKey ? {_key: keyGenerator()} : {}), - } - } - if (operation.type === 'insert_node') { - // Must be given a new key or adding/removing marks while typing gets in trouble (duped keys)! - const withNewKey = !isPreservingKeys(editor) || !('_key' in operation.node) - if (!Editor.isEditor(operation.node)) { - operation.node = { - ...operation.node, - ...(withNewKey ? {_key: keyGenerator()} : {}), - } - } - } - apply(operation) - } - editor.normalizeNode = (entry) => { - const [node, path] = entry - if (Element.isElement(node) && node._type === schemaTypes.block.name) { - // Set key on block itself - if (!node._key) { - Transforms.setNodes(editor, {_key: keyGenerator()}, {at: path}) - } - // Set keys on it's children - for (const [child, childPath] of Node.children(editor, path)) { - if (!child._key) { - Transforms.setNodes(editor, {_key: keyGenerator()}, {at: childPath}) - return - } - } - } - normalizeNode(entry) - } - - return editor - } -} diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPatches.ts b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPatches.ts deleted file mode 100644 index 67f0001c0e0..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPatches.ts +++ /dev/null @@ -1,274 +0,0 @@ -/* eslint-disable max-nested-callbacks */ -import {type Subject} from 'rxjs' -import { - type Descendant, - Editor, - type InsertNodeOperation, - type InsertTextOperation, - type MergeNodeOperation, - type MoveNodeOperation, - type Operation, - type RemoveNodeOperation, - type RemoveTextOperation, - type SetNodeOperation, - type SplitNodeOperation, -} from 'slate' - -import {insert, setIfMissing, unset} from '../../patch/PatchEvent' -import { - type EditorChange, - type PatchObservable, - type PortableTextMemberSchemaTypes, - type PortableTextSlateEditor, -} from '../../types/editor' -import {type Patch} from '../../types/patch' -import {createApplyPatch} from '../../utils/applyPatch' -import {debugWithName} from '../../utils/debug' -import {fromSlateValue, isEqualToEmptyEditor} from '../../utils/values' -import {IS_PROCESSING_REMOTE_CHANGES, KEY_TO_VALUE_ELEMENT} from '../../utils/weakMaps' -import {withRemoteChanges} from '../../utils/withChanges' -import {isPatching, PATCHING, withoutPatching} from '../../utils/withoutPatching' -import {withPreserveKeys} from '../../utils/withPreserveKeys' -import {withoutSaving} from './createWithUndoRedo' - -const debug = debugWithName('plugin:withPatches') -const debugVerbose = false - -export interface PatchFunctions { - insertNodePatch: ( - editor: PortableTextSlateEditor, - operation: InsertNodeOperation, - previousChildren: Descendant[], - ) => Patch[] - insertTextPatch: ( - editor: PortableTextSlateEditor, - operation: InsertTextOperation, - previousChildren: Descendant[], - ) => Patch[] - mergeNodePatch: ( - editor: PortableTextSlateEditor, - operation: MergeNodeOperation, - previousChildren: Descendant[], - ) => Patch[] - moveNodePatch: ( - editor: PortableTextSlateEditor, - operation: MoveNodeOperation, - previousChildren: Descendant[], - ) => Patch[] - removeNodePatch: ( - editor: PortableTextSlateEditor, - operation: RemoveNodeOperation, - previousChildren: Descendant[], - ) => Patch[] - removeTextPatch: ( - editor: PortableTextSlateEditor, - operation: RemoveTextOperation, - previousChildren: Descendant[], - ) => Patch[] - setNodePatch: ( - editor: PortableTextSlateEditor, - operation: SetNodeOperation, - previousChildren: Descendant[], - ) => Patch[] - splitNodePatch: ( - editor: PortableTextSlateEditor, - operation: SplitNodeOperation, - previousChildren: Descendant[], - ) => Patch[] -} - -interface Options { - change$: Subject - keyGenerator: () => string - patches$?: PatchObservable - patchFunctions: PatchFunctions - readOnly: boolean - schemaTypes: PortableTextMemberSchemaTypes -} - -export function createWithPatches({ - change$, - patches$, - patchFunctions, - readOnly, - schemaTypes, -}: Options): (editor: PortableTextSlateEditor) => PortableTextSlateEditor { - // The previous editor children are needed to figure out the _key of deleted nodes - // The editor.children would no longer contain that information if the node is already deleted. - let previousChildren: Descendant[] - - const applyPatch = createApplyPatch(schemaTypes) - - return function withPatches(editor: PortableTextSlateEditor) { - IS_PROCESSING_REMOTE_CHANGES.set(editor, false) - PATCHING.set(editor, true) - previousChildren = [...editor.children] - - const {apply} = editor - let bufferedPatches: Patch[] = [] - - const handleBufferedRemotePatches = () => { - if (bufferedPatches.length === 0) { - return - } - const patches = bufferedPatches - bufferedPatches = [] - let changed = false - withRemoteChanges(editor, () => { - Editor.withoutNormalizing(editor, () => { - withoutPatching(editor, () => { - withoutSaving(editor, () => { - withPreserveKeys(editor, () => { - patches.forEach((patch) => { - if (debug.enabled) debug(`Handling remote patch ${JSON.stringify(patch)}`) - changed = applyPatch(editor, patch) - }) - }) - }) - }) - }) - if (changed) { - editor.normalize() - editor.onChange() - } - }) - } - - const handlePatches = ({patches}: {patches: Patch[]}) => { - const remotePatches = patches.filter((p) => p.origin !== 'local') - if (remotePatches.length === 0) { - return - } - bufferedPatches = bufferedPatches.concat(remotePatches) - handleBufferedRemotePatches() - } - - if (patches$) { - editor.subscriptions.push(() => { - debug('Subscribing to patches$') - const sub = patches$.subscribe(handlePatches) - return () => { - debug('Unsubscribing to patches$') - sub.unsubscribe() - } - }) - } - - editor.apply = (operation: Operation): void | Editor => { - if (readOnly) { - apply(operation) - return editor - } - let patches: Patch[] = [] - - // Update previous children here before we apply - previousChildren = editor.children - - const editorWasEmpty = isEqualToEmptyEditor(previousChildren, schemaTypes) - - // Apply the operation - apply(operation) - - const editorIsEmpty = isEqualToEmptyEditor(editor.children, schemaTypes) - - if (!isPatching(editor)) { - if (debugVerbose && debug.enabled) - debug(`Editor is not producing patch for operation ${operation.type}`, operation) - return editor - } - - // If the editor was empty and now isn't, insert the placeholder into it. - if (editorWasEmpty && !editorIsEmpty && operation.type !== 'set_selection') { - patches.push(insert(previousChildren, 'before', [0])) - } - - switch (operation.type) { - case 'insert_text': - patches = [ - ...patches, - ...patchFunctions.insertTextPatch(editor, operation, previousChildren), - ] - break - case 'remove_text': - patches = [ - ...patches, - ...patchFunctions.removeTextPatch(editor, operation, previousChildren), - ] - break - case 'remove_node': - patches = [ - ...patches, - ...patchFunctions.removeNodePatch(editor, operation, previousChildren), - ] - break - case 'split_node': - patches = [ - ...patches, - ...patchFunctions.splitNodePatch(editor, operation, previousChildren), - ] - break - case 'insert_node': - patches = [ - ...patches, - ...patchFunctions.insertNodePatch(editor, operation, previousChildren), - ] - break - case 'set_node': - patches = [ - ...patches, - ...patchFunctions.setNodePatch(editor, operation, previousChildren), - ] - break - case 'merge_node': - patches = [ - ...patches, - ...patchFunctions.mergeNodePatch(editor, operation, previousChildren), - ] - break - case 'move_node': - patches = [ - ...patches, - ...patchFunctions.moveNodePatch(editor, operation, previousChildren), - ] - break - case 'set_selection': - default: - // Do nothing - } - - // Unset the value if a operation made the editor empty - if ( - !editorWasEmpty && - editorIsEmpty && - ['merge_node', 'set_node', 'remove_text', 'remove_node'].includes(operation.type) - ) { - patches = [...patches, unset([])] - change$.next({ - type: 'unset', - previousValue: fromSlateValue( - previousChildren, - schemaTypes.block.name, - KEY_TO_VALUE_ELEMENT.get(editor), - ), - }) - } - - // Prepend patches with setIfMissing if going from empty editor to something involving a patch. - if (editorWasEmpty && patches.length > 0) { - patches = [setIfMissing([], []), ...patches] - } - - // Emit all patches - if (patches.length > 0) { - patches.forEach((patch) => { - change$.next({ - type: 'patch', - patch: {...patch, origin: 'local'}, - }) - }) - } - return editor - } - return editor - } -} diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPlaceholderBlock.ts b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPlaceholderBlock.ts deleted file mode 100644 index f99cf732b4f..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPlaceholderBlock.ts +++ /dev/null @@ -1,36 +0,0 @@ -import {Editor, Path} from 'slate' - -import {type PortableTextSlateEditor} from '../../types/editor' -import {type SlateTextBlock, type VoidElement} from '../../types/slate' -import {debugWithName} from '../../utils/debug' - -const debug = debugWithName('plugin:withPlaceholderBlock') - -/** - * Keep a "placeholder" block present when the editor is empty - * - */ -export function createWithPlaceholderBlock(): ( - editor: PortableTextSlateEditor, -) => PortableTextSlateEditor { - return function withPlaceholderBlock(editor: PortableTextSlateEditor): PortableTextSlateEditor { - const {apply} = editor - - editor.apply = (op) => { - if (op.type === 'remove_node') { - const node = op.node as SlateTextBlock | VoidElement - if (op.path[0] === 0 && Editor.isVoid(editor, node)) { - // Check next path, if it exists, do nothing - const nextPath = Path.next(op.path) - // Is removing the first block which is a void (not a text block), add a new empty text block in it, if there is no other element in the next path - if (!editor.children[nextPath[0]]) { - debug('Adding placeholder block') - Editor.insertNode(editor, editor.pteCreateEmptyBlock()) - } - } - } - apply(op) - } - return editor - } -} diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextBlockStyle.ts b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextBlockStyle.ts deleted file mode 100644 index d5595636c6e..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextBlockStyle.ts +++ /dev/null @@ -1,91 +0,0 @@ -import {Editor, type Node, Path, Text as SlateText, Transforms} from 'slate' - -import {type PortableTextMemberSchemaTypes, type PortableTextSlateEditor} from '../../types/editor' -import {debugWithName} from '../../utils/debug' - -const debug = debugWithName('plugin:withPortableTextBlockStyle') - -export function createWithPortableTextBlockStyle( - types: PortableTextMemberSchemaTypes, -): (editor: PortableTextSlateEditor) => PortableTextSlateEditor { - const defaultStyle = types.styles[0].value - return function withPortableTextBlockStyle( - editor: PortableTextSlateEditor, - ): PortableTextSlateEditor { - // Extend Slate's default normalization to reset split node to normal style - // if there is no text at the right end of the split. - const {normalizeNode} = editor - editor.normalizeNode = (nodeEntry) => { - normalizeNode(nodeEntry) - const [, path] = nodeEntry - for (const op of editor.operations) { - if ( - op.type === 'split_node' && - op.path.length === 1 && - editor.isTextBlock(op.properties) && - op.properties.style !== defaultStyle && - op.path[0] === path[0] && - !Path.equals(path, op.path) - ) { - const [child] = Editor.node(editor, [op.path[0] + 1, 0]) - if (SlateText.isText(child) && child.text === '') { - debug(`Normalizing split node to ${defaultStyle} style`, op) - Transforms.setNodes(editor, {style: defaultStyle}, {at: [op.path[0] + 1], voids: false}) - break - } - } - } - } - editor.pteHasBlockStyle = (style: string): boolean => { - if (!editor.selection) { - return false - } - const selectedBlocks = [ - ...Editor.nodes(editor, { - at: editor.selection, - match: (node) => editor.isTextBlock(node) && node.style === style, - }), - ] - if (selectedBlocks.length > 0) { - return true - } - return false - } - - editor.pteToggleBlockStyle = (blockStyle: string): void => { - if (!editor.selection) { - return - } - const selectedBlocks = [ - ...Editor.nodes(editor, { - at: editor.selection, - match: (node) => editor.isTextBlock(node), - }), - ] - selectedBlocks.forEach(([node, path]) => { - if (editor.isTextBlock(node) && node.style === blockStyle) { - debug(`Unsetting block style '${blockStyle}'`) - Transforms.setNodes(editor, {...node, style: defaultStyle} as Partial, { - at: path, - }) - } else { - if (blockStyle) { - debug(`Setting style '${blockStyle}'`) - } else { - debug('Setting default style', defaultStyle) - } - Transforms.setNodes( - editor, - { - ...node, - style: blockStyle || defaultStyle, - } as Partial, - {at: path}, - ) - } - }) - editor.onChange() - } - return editor - } -} diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextLists.ts b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextLists.ts deleted file mode 100644 index 6f15eb4c39d..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextLists.ts +++ /dev/null @@ -1,160 +0,0 @@ -import {Editor, Element, type Node, Text, Transforms} from 'slate' - -import {type PortableTextMemberSchemaTypes, type PortableTextSlateEditor} from '../../types/editor' -import {debugWithName} from '../../utils/debug' - -const debug = debugWithName('plugin:withPortableTextLists') -const MAX_LIST_LEVEL = 10 - -export function createWithPortableTextLists(types: PortableTextMemberSchemaTypes) { - return function withPortableTextLists(editor: PortableTextSlateEditor): PortableTextSlateEditor { - editor.pteToggleListItem = (listItemStyle: string) => { - const isActive = editor.pteHasListStyle(listItemStyle) - if (isActive) { - debug(`Remove list item '${listItemStyle}'`) - editor.pteUnsetListItem(listItemStyle) - } else { - debug(`Add list item '${listItemStyle}'`) - editor.pteSetListItem(listItemStyle) - } - } - - editor.pteUnsetListItem = (listItemStyle: string) => { - if (!editor.selection) { - return - } - const selectedBlocks = [ - ...Editor.nodes(editor, { - at: editor.selection, - match: (node) => Element.isElement(node) && node._type === types.block.name, - }), - ] - selectedBlocks.forEach(([node, path]) => { - if (editor.isListBlock(node)) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const {listItem, level, ...rest} = node - const newNode = { - ...rest, - listItem: undefined, - level: undefined, - } as Partial - debug(`Unsetting list '${listItemStyle}'`) - Transforms.setNodes(editor, newNode, {at: path}) - } - }) - } - - editor.pteSetListItem = (listItemStyle: string) => { - if (!editor.selection) { - return - } - const selectedBlocks = [ - ...Editor.nodes(editor, { - at: editor.selection, - match: (node) => editor.isTextBlock(node), - }), - ] - selectedBlocks.forEach(([node, path]) => { - debug(`Setting list '${listItemStyle}'`) - Transforms.setNodes( - editor, - { - ...node, - level: 1, - listItem: listItemStyle || (types.lists[0] && types.lists[0].value), - } as Partial, - {at: path}, - ) - }) - } - - editor.pteEndList = () => { - if (!editor.selection) { - return false - } - const selectedBlocks = [ - ...Editor.nodes(editor, { - at: editor.selection, - match: (node) => - Element.isElement(node) && - editor.isListBlock(node) && - node.children.length === 1 && - Text.isText(node.children[0]) && - node.children[0].text === '', - }), - ] - if (selectedBlocks.length === 0) { - return false - } - selectedBlocks.forEach(([node, path]) => { - if (Element.isElement(node)) { - debug('Unset list') - Transforms.setNodes( - editor, - { - ...node, - level: undefined, - listItem: undefined, - }, - {at: path}, - ) - } - }) - return true // Note: we are exiting the plugin chain by not returning editor (or hotkey plugin 'enter' will fire) - } - - editor.pteIncrementBlockLevels = (reverse?: boolean): boolean => { - if (!editor.selection) { - return false - } - const selectedBlocks = [ - ...Editor.nodes(editor, { - at: editor.selection, - match: (node) => !!editor.isListBlock(node), - }), - ] - if (selectedBlocks.length === 0) { - return false - } - selectedBlocks.forEach(([node, path]) => { - if (editor.isListBlock(node)) { - let level = node.level || 1 - if (reverse) { - level-- - debug('Decrementing list level', Math.min(MAX_LIST_LEVEL, Math.max(1, level))) - } else { - level++ - debug('Incrementing list level', Math.min(MAX_LIST_LEVEL, Math.max(1, level))) - } - Transforms.setNodes( - editor, - {level: Math.min(MAX_LIST_LEVEL, Math.max(1, level))}, - {at: path}, - ) - } - }) - return true - } - - editor.pteHasListStyle = (listStyle: string): boolean => { - if (!editor.selection) { - return false - } - const selectedBlocks = [ - ...Editor.nodes(editor, { - at: editor.selection, - match: (node) => editor.isTextBlock(node), - }), - ] - - if (selectedBlocks.length > 0) { - return selectedBlocks.every( - ([node]) => editor.isListBlock(node) && node.listItem === listStyle, - ) - } - return false - } - - return editor - } -} diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextMarkModel.ts b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextMarkModel.ts deleted file mode 100644 index ac555b2ac9b..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextMarkModel.ts +++ /dev/null @@ -1,441 +0,0 @@ -/* eslint-disable max-statements */ -/* eslint-disable complexity */ -/** - * - * This plugin will change Slate's default marks model (every prop is a mark) with the Portable Text model (marks is an array of strings on prop .marks). - * - */ - -import {isEqual, uniq} from 'lodash' -import {type Subject} from 'rxjs' -import {type Descendant, Editor, Element, Path, Range, Text, Transforms} from 'slate' - -import { - type EditorChange, - type PortableTextMemberSchemaTypes, - type PortableTextSlateEditor, -} from '../../types/editor' -import {debugWithName} from '../../utils/debug' -import {toPortableTextRange} from '../../utils/ranges' -import {EMPTY_MARKS} from '../../utils/values' - -const debug = debugWithName('plugin:withPortableTextMarkModel') - -export function createWithPortableTextMarkModel( - types: PortableTextMemberSchemaTypes, - change$: Subject, -): (editor: PortableTextSlateEditor) => PortableTextSlateEditor { - return function withPortableTextMarkModel(editor: PortableTextSlateEditor) { - const {apply, normalizeNode} = editor - const decorators = types.decorators.map((t) => t.value) - - // Selections are normally emitted automatically via - // onChange, but they will keep the object reference if - // the selection is the same as the previous. - // When toggling marks however, it might not even - // result in a onChange event (for instance when nothing is selected), - // and if you toggle marks on a block with one single span, - // the selection would also stay the same. - // We should force a new selection object here when toggling marks, - // because toolbars and other things can very conveniently - // be memo'ed on the editor selection to update itself. - const forceNewSelection = () => { - if (editor.selection) { - Transforms.select(editor, {...editor.selection}) - editor.selection = {...editor.selection} // Ensure new object - } - const ptRange = toPortableTextRange(editor.children, editor.selection, types) - change$.next({type: 'selection', selection: ptRange}) - } - - // Extend Slate's default normalization. Merge spans with same set of .marks when doing merge_node operations, and clean up markDefs / marks - editor.normalizeNode = (nodeEntry) => { - normalizeNode(nodeEntry) - if ( - editor.operations.some((op) => - [ - 'insert_node', - 'insert_text', - 'merge_node', - 'remove_node', - 'remove_text', - 'set_node', - ].includes(op.type), - ) - ) { - mergeSpans(editor) - } - const [node, path] = nodeEntry - const isSpan = Text.isText(node) && node._type === types.span.name - const isTextBlock = editor.isTextBlock(node) - if (isSpan || isTextBlock) { - if (isSpan && !Array.isArray(node.marks)) { - debug('Adding .marks to span node') - Transforms.setNodes(editor, {marks: []}, {at: path}) - editor.onChange() - } - const hasSpanMarks = isSpan && (node.marks || []).length > 0 - if (hasSpanMarks) { - const spanMarks = node.marks || EMPTY_MARKS - // Test that every annotation mark used has a definition in markDefs - const annotationMarks = spanMarks.filter( - (mark) => !types.decorators.map((dec) => dec.value).includes(mark), - ) - if (annotationMarks.length > 0) { - const [block] = Editor.node(editor, Path.parent(path)) - const orphanedMarks = - (editor.isTextBlock(block) && - annotationMarks.filter( - (mark) => !block.markDefs?.find((def) => def._key === mark), - )) || - [] - if (orphanedMarks.length > 0) { - debug('Removing orphaned .marks from span node') - Transforms.setNodes( - editor, - {marks: spanMarks.filter((mark) => !orphanedMarks.includes(mark))}, - {at: path}, - ) - editor.onChange() - } - } - } - for (const op of editor.operations) { - // Make sure markDefs are copied over when merging two blocks. - if ( - op.type === 'merge_node' && - op.path.length === 1 && - 'markDefs' in op.properties && - op.properties._type === types.block.name && - Array.isArray(op.properties.markDefs) && - op.properties.markDefs.length > 0 && - op.path[0] - 1 >= 0 - ) { - const [targetBlock, targetPath] = Editor.node(editor, [op.path[0] - 1]) - debug(`Copying markDefs over to merged block`, op) - if (editor.isTextBlock(targetBlock)) { - const oldDefs = (Array.isArray(targetBlock.markDefs) && targetBlock.markDefs) || [] - const newMarkDefs = uniq([...oldDefs, ...op.properties.markDefs]) - const isNormalized = isEqual(newMarkDefs, targetBlock.markDefs) - // eslint-disable-next-line max-depth - if (!isNormalized) { - Transforms.setNodes(editor, {markDefs: newMarkDefs}, {at: targetPath, voids: false}) - editor.onChange() - } - } - } - // Make sure markDefs are copied over to new block when splitting a block. - if ( - op.type === 'split_node' && - op.path.length === 1 && - Element.isElementProps(op.properties) && - op.properties._type === types.block.name && - 'markDefs' in op.properties && - Array.isArray(op.properties.markDefs) && - op.properties.markDefs.length > 0 && - op.path[0] + 1 < editor.children.length - ) { - const [targetBlock, targetPath] = Editor.node(editor, [op.path[0] + 1]) - debug(`Copying markDefs over to split block`, op) - if (editor.isTextBlock(targetBlock)) { - const oldDefs = (Array.isArray(targetBlock.markDefs) && targetBlock.markDefs) || [] - Transforms.setNodes( - editor, - {markDefs: uniq([...oldDefs, ...op.properties.markDefs])}, - {at: targetPath, voids: false}, - ) - editor.onChange() - } - } - // Make sure marks are reset, if a block is split at the end. - if ( - op.type === 'split_node' && - op.path.length === 2 && - (op.properties as unknown as Descendant)._type === types.span.name && - 'marks' in op.properties && - Array.isArray(op.properties.marks) && - op.properties.marks.length > 0 && - op.path[0] + 1 < editor.children.length - ) { - const [child, childPath] = Editor.node(editor, [op.path[0] + 1, 0]) - if ( - Text.isText(child) && - child.text === '' && - Array.isArray(child.marks) && - child.marks.length > 0 - ) { - Transforms.setNodes(editor, {marks: []}, {at: childPath, voids: false}) - editor.onChange() - } - } - // Make sure markDefs are reset, if a block is split at start. - if ( - op.type === 'split_node' && - op.path.length === 1 && - (op.properties as unknown as Descendant)._type === types.block.name && - 'markDefs' in op.properties && - Array.isArray(op.properties.markDefs) && - op.properties.markDefs.length > 0 - ) { - const [block, blockPath] = Editor.node(editor, [op.path[0]]) - if ( - editor.isTextBlock(block) && - block.children.length === 1 && - block.markDefs && - block.markDefs.length > 0 && - Text.isText(block.children[0]) && - block.children[0].text === '' && - (!block.children[0].marks || block.children[0].marks.length === 0) - ) { - Transforms.setNodes(editor, {markDefs: []}, {at: blockPath}) - editor.onChange() - } - } - } - // Empty marks if text is empty - if ( - isSpan && - Array.isArray(node.marks) && - (!node.marks || (node.marks.length > 0 && node.text === '')) - ) { - Transforms.setNodes(editor, {marks: []}, {at: path, voids: false}) - editor.onChange() - } - } - // Check consistency of markDefs (unless we are merging two nodes) - if ( - editor.isTextBlock(node) && - !editor.operations.some( - (op) => op.type === 'merge_node' && 'markDefs' in op.properties && op.path.length === 1, - ) - ) { - const newMarkDefs = (node.markDefs || []).filter((def) => { - return node.children.find((child) => { - return ( - Text.isText(child) && Array.isArray(child.marks) && child.marks.includes(def._key) - ) - }) - }) - if (node.markDefs && !isEqual(newMarkDefs, node.markDefs)) { - debug('Removing markDef not in use') - Transforms.setNodes( - editor, - { - markDefs: newMarkDefs, - }, - {at: path}, - ) - editor.onChange() - } - } - } - - // Special hook before inserting text at the end of an annotation. - editor.apply = (op) => { - if (op.type === 'insert_text') { - const {selection} = editor - if ( - selection && - Range.isCollapsed(selection) && - Editor.marks(editor)?.marks?.some((mark) => !decorators.includes(mark)) - ) { - const [node] = Array.from( - Editor.nodes(editor, { - mode: 'lowest', - at: selection.focus, - match: (n) => (n as unknown as Descendant)._type === types.span.name, - voids: false, - }), - )[0] || [undefined] - if ( - Text.isText(node) && - node.text.length === selection.focus.offset && - Array.isArray(node.marks) && - node.marks.length > 0 - ) { - apply(op) - Transforms.splitNodes(editor, { - match: Text.isText, - at: {...selection.focus, offset: selection.focus.offset}, - }) - const marksWithoutAnnotationMarks: string[] = ( - { - ...(Editor.marks(editor) || {}), - }.marks || [] - ).filter((mark) => decorators.includes(mark)) - Transforms.setNodes( - editor, - {marks: marksWithoutAnnotationMarks}, - {at: Path.next(selection.focus.path)}, - ) - debug('Inserting text at end of annotation') - return - } - } - } - apply(op) - } - - // Override built in addMark function - editor.addMark = (mark: string) => { - if (editor.selection) { - if (Range.isExpanded(editor.selection)) { - // Split if needed - Transforms.setNodes(editor, {}, {match: Text.isText, split: true}) - // Use new selection - const splitTextNodes = [ - ...Editor.nodes(editor, {at: editor.selection, match: Text.isText}), - ] - const shouldRemoveMark = splitTextNodes.every((node) => node[0].marks?.includes(mark)) - - if (shouldRemoveMark) { - editor.removeMark(mark) - return editor - } - Editor.withoutNormalizing(editor, () => { - splitTextNodes.forEach(([node, path]) => { - const marks = [ - ...(Array.isArray(node.marks) ? node.marks : []).filter( - (eMark: string) => eMark !== mark, - ), - mark, - ] - Transforms.setNodes( - editor, - {marks}, - {at: path, match: Text.isText, split: true, hanging: true}, - ) - }) - }) - Editor.normalize(editor) - } else { - const existingMarks: string[] = - { - ...(Editor.marks(editor) || {}), - }.marks || [] - const marks = { - ...(Editor.marks(editor) || {}), - marks: [...existingMarks, mark], - } - editor.marks = marks as Text - forceNewSelection() - return editor - } - editor.onChange() - forceNewSelection() - } - return editor - } - - // Override built in removeMark function - editor.removeMark = (mark: string) => { - const {selection} = editor - if (selection) { - if (Range.isExpanded(selection)) { - Editor.withoutNormalizing(editor, () => { - // Split if needed - Transforms.setNodes(editor, {}, {match: Text.isText, split: true}) - if (editor.selection) { - const splitTextNodes = [ - ...Editor.nodes(editor, {at: editor.selection, match: Text.isText}), - ] - splitTextNodes.forEach(([node, path]) => { - const block = editor.children[path[0]] - if (Element.isElement(block) && block.children.includes(node)) { - Transforms.setNodes( - editor, - { - marks: (Array.isArray(node.marks) ? node.marks : []).filter( - (eMark: string) => eMark !== mark, - ), - _type: 'span', - }, - {at: path}, - ) - } - }) - } - }) - Editor.normalize(editor) - } else { - const existingMarks: string[] = - { - ...(Editor.marks(editor) || {}), - }.marks || [] - const marks = { - ...(Editor.marks(editor) || {}), - marks: existingMarks.filter((eMark) => eMark !== mark), - } as Text - editor.marks = {marks: marks.marks, _type: 'span'} as Text - forceNewSelection() - return editor - } - editor.onChange() - forceNewSelection() - } - return editor - } - - editor.pteIsMarkActive = (mark: string): boolean => { - if (!editor.selection) { - return false - } - - const selectedNodes = Array.from( - Editor.nodes(editor, {match: Text.isText, at: editor.selection}), - ) - - if (Range.isExpanded(editor.selection)) { - return selectedNodes.every((n) => { - const [node] = n - - return node.marks?.includes(mark) - }) - } - - return ( - { - ...(Editor.marks(editor) || {}), - }.marks || [] - ).includes(mark) - } - - // Custom editor function to toggle a mark - editor.pteToggleMark = (mark: string) => { - const isActive = editor.pteIsMarkActive(mark) - if (isActive) { - debug(`Remove mark '${mark}'`) - Editor.removeMark(editor, mark) - } else { - debug(`Add mark '${mark}'`) - Editor.addMark(editor, mark, true) - } - } - return editor - } - - /** - * Normalize re-marked spans in selection - */ - function mergeSpans(editor: PortableTextSlateEditor) { - const {selection} = editor - if (selection) { - for (const [node, path] of Array.from( - Editor.nodes(editor, { - at: Editor.range(editor, [selection.anchor.path[0]], [selection.focus.path[0]]), - }), - ).reverse()) { - const [parent] = path.length > 1 ? Editor.node(editor, Path.parent(path)) : [undefined] - const nextPath = [path[0], path[1] + 1] - if (editor.isTextBlock(parent)) { - const nextNode = parent.children[nextPath[1]] - if (Text.isText(node) && Text.isText(nextNode) && isEqual(nextNode.marks, node.marks)) { - debug('Merging spans') - Transforms.mergeNodes(editor, {at: nextPath, voids: true}) - editor.onChange() - } - } - } - } - } -} diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextSelections.ts b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextSelections.ts deleted file mode 100644 index aac9dd423b8..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextSelections.ts +++ /dev/null @@ -1,65 +0,0 @@ -import {type Subject} from 'rxjs' -import {type BaseRange} from 'slate' - -import { - type EditorChange, - type EditorSelection, - type PortableTextMemberSchemaTypes, - type PortableTextSlateEditor, -} from '../../types/editor' -import {debugWithName} from '../../utils/debug' -import {type ObjectWithKeyAndType, toPortableTextRange} from '../../utils/ranges' -import {SLATE_TO_PORTABLE_TEXT_RANGE} from '../../utils/weakMaps' - -const debug = debugWithName('plugin:withPortableTextSelections') -const debugVerbose = debug.enabled && false - -// This plugin will make sure that we emit a PT selection whenever the editor has changed. -export function createWithPortableTextSelections( - change$: Subject, - types: PortableTextMemberSchemaTypes, -): (editor: PortableTextSlateEditor) => PortableTextSlateEditor { - let prevSelection: BaseRange | null = null - return function withPortableTextSelections( - editor: PortableTextSlateEditor, - ): PortableTextSlateEditor { - const emitPortableTextSelection = () => { - if (prevSelection !== editor.selection) { - let ptRange: EditorSelection = null - if (editor.selection) { - const existing = SLATE_TO_PORTABLE_TEXT_RANGE.get(editor.selection) - if (existing) { - ptRange = existing - } else { - const value = editor.children satisfies ObjectWithKeyAndType[] - ptRange = toPortableTextRange(value, editor.selection, types) - SLATE_TO_PORTABLE_TEXT_RANGE.set(editor.selection, ptRange) - } - } - if (debugVerbose) { - debug( - `Emitting selection ${JSON.stringify(ptRange || null)} (${JSON.stringify( - editor.selection, - )})`, - ) - } - if (ptRange) { - change$.next({type: 'selection', selection: ptRange}) - } else { - change$.next({type: 'selection', selection: null}) - } - } - prevSelection = editor.selection - } - - const {onChange} = editor - editor.onChange = () => { - const hasChanges = editor.operations.length > 0 - onChange() - if (hasChanges) { - emitPortableTextSelection() - } - } - return editor - } -} diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithSchemaTypes.ts b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithSchemaTypes.ts deleted file mode 100644 index 8b35b1e2c36..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithSchemaTypes.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { - isPortableTextListBlock, - isPortableTextSpan, - isPortableTextTextBlock, - type PortableTextListBlock, - type PortableTextSpan, - type PortableTextTextBlock, -} from '@sanity/types' -import {type Element, Transforms} from 'slate' - -import {type PortableTextMemberSchemaTypes, type PortableTextSlateEditor} from '../../types/editor' -import {debugWithName} from '../../utils/debug' - -const debug = debugWithName('plugin:withSchemaTypes') -/** - * This plugin makes sure that schema types are recognized properly by Slate as blocks, voids, inlines - * - */ -export function createWithSchemaTypes({ - schemaTypes, - keyGenerator, -}: { - schemaTypes: PortableTextMemberSchemaTypes - keyGenerator: () => string -}) { - return function withSchemaTypes(editor: PortableTextSlateEditor): PortableTextSlateEditor { - editor.isTextBlock = (value: unknown): value is PortableTextTextBlock => { - return isPortableTextTextBlock(value) && value._type === schemaTypes.block.name - } - editor.isTextSpan = (value: unknown): value is PortableTextSpan => { - return isPortableTextSpan(value) && value._type == schemaTypes.span.name - } - editor.isListBlock = (value: unknown): value is PortableTextListBlock => { - return isPortableTextListBlock(value) && value._type === schemaTypes.block.name - } - editor.isVoid = (element: Element): boolean => { - return ( - schemaTypes.block.name !== element._type && - (schemaTypes.blockObjects.map((obj) => obj.name).includes(element._type) || - schemaTypes.inlineObjects.map((obj) => obj.name).includes(element._type)) - ) - } - editor.isInline = (element: Element): boolean => { - const inlineSchemaTypes = schemaTypes.inlineObjects.map((obj) => obj.name) - return ( - inlineSchemaTypes.includes(element._type) && - '__inline' in element && - element.__inline === true - ) - } - - // Extend Slate's default normalization - const {normalizeNode} = editor - editor.normalizeNode = (entry) => { - const [node, path] = entry - - // If text block children node is missing _type, set it to the span type - if (node._type === undefined && path.length === 2) { - debug('Setting span type on text node without a type') - const span = node as PortableTextSpan - const key = span._key || keyGenerator() - Transforms.setNodes(editor, {...span, _type: schemaTypes.span.name, _key: key}, {at: path}) - } - - // catches cases when the children are missing keys but excludes it when the normalize is running the node as the editor object - if (node._key === undefined && (path.length === 1 || path.length === 2)) { - debug('Setting missing key on child node without a key') - const key = keyGenerator() - Transforms.setNodes(editor, {_key: key}, {at: path}) - } - - normalizeNode(entry) - } - return editor - } -} diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithUndoRedo.ts b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithUndoRedo.ts deleted file mode 100644 index 87e13b5884d..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithUndoRedo.ts +++ /dev/null @@ -1,494 +0,0 @@ -/** - * This plugin will make the editor support undo/redo on the local state only. - * The undo/redo steps are rebased against incoming patches since the step occurred. - */ - -import {DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, parsePatch} from '@sanity/diff-match-patch' -import {type ObjectSchemaType, type PortableTextBlock} from '@sanity/types' -import {flatten, isEqual} from 'lodash' -import {type Descendant, Editor, Operation, Path, type SelectionOperation, Transforms} from 'slate' - -import {type PatchObservable, type PortableTextSlateEditor} from '../../types/editor' -import {type Patch} from '../../types/patch' -import {debugWithName} from '../../utils/debug' -import {fromSlateValue} from '../../utils/values' -import {withPreserveKeys} from '../../utils/withPreserveKeys' - -const debug = debugWithName('plugin:withUndoRedo') -const debugVerbose = debug.enabled && false - -const SAVING = new WeakMap() -const REMOTE_PATCHES = new WeakMap< - Editor, - { - patch: Patch - time: Date - snapshot: PortableTextBlock[] | undefined - previousSnapshot: PortableTextBlock[] | undefined - }[] ->() -const UNDO_STEP_LIMIT = 1000 - -const isSaving = (editor: Editor): boolean | undefined => { - const state = SAVING.get(editor) - return state === undefined ? true : state -} - -export interface Options { - patches$?: PatchObservable - readOnly: boolean - blockSchemaType: ObjectSchemaType -} - -const getRemotePatches = (editor: Editor) => { - if (!REMOTE_PATCHES.get(editor)) { - REMOTE_PATCHES.set(editor, []) - } - return REMOTE_PATCHES.get(editor) || [] -} - -export function createWithUndoRedo( - options: Options, -): (editor: PortableTextSlateEditor) => PortableTextSlateEditor { - const {readOnly, patches$, blockSchemaType} = options - - return (editor: PortableTextSlateEditor) => { - let previousSnapshot: PortableTextBlock[] | undefined = fromSlateValue( - editor.children, - blockSchemaType.name, - ) - const remotePatches = getRemotePatches(editor) - if (patches$) { - editor.subscriptions.push(() => { - debug('Subscribing to patches') - const sub = patches$.subscribe(({patches, snapshot}) => { - let reset = false - patches.forEach((patch) => { - if (!reset && patch.origin !== 'local' && remotePatches) { - if (patch.type === 'unset' && patch.path.length === 0) { - debug('Someone else cleared the content, resetting undo/redo history') - editor.history = {undos: [], redos: []} - remotePatches.splice(0, remotePatches.length) - SAVING.set(editor, true) - reset = true - return - } - remotePatches.push({patch, time: new Date(), snapshot, previousSnapshot}) - } - }) - previousSnapshot = snapshot - }) - return () => { - debug('Unsubscribing to patches') - sub.unsubscribe() - } - }) - } - editor.history = {undos: [], redos: []} - const {apply} = editor - editor.apply = (op: Operation) => { - if (readOnly) { - apply(op) - return - } - const {operations, history} = editor - const {undos} = history - const step = undos[undos.length - 1] - const lastOp = step && step.operations && step.operations[step.operations.length - 1] - const overwrite = shouldOverwrite(op, lastOp) - const save = isSaving(editor) - - let merge = true - if (save) { - if (!step) { - merge = false - } else if (operations.length === 0) { - merge = shouldMerge(op, lastOp) || overwrite - } - - if (step && merge) { - step.operations.push(op) - } else { - const newStep = { - operations: [...(editor.selection === null ? [] : [createSelectOperation(editor)]), op], - timestamp: new Date(), - } - undos.push(newStep) - debug('Created new undo step', step) - } - - while (undos.length > UNDO_STEP_LIMIT) { - undos.shift() - } - - if (shouldClear(op)) { - history.redos = [] - } - } - apply(op) - } - - editor.undo = () => { - if (readOnly) { - return - } - const {undos} = editor.history - if (undos.length > 0) { - const step = undos[undos.length - 1] - debug('Undoing', step) - if (step.operations.length > 0) { - const otherPatches = remotePatches.filter((item) => item.time >= step.timestamp) - let transformedOperations = step.operations - otherPatches.forEach((item) => { - transformedOperations = flatten( - transformedOperations.map((op) => - transformOperation(editor, item.patch, op, item.snapshot, item.previousSnapshot), - ), - ) - }) - try { - Editor.withoutNormalizing(editor, () => { - withPreserveKeys(editor, () => { - withoutSaving(editor, () => { - transformedOperations - .map(Operation.inverse) - .reverse() - // eslint-disable-next-line max-nested-callbacks - .forEach((op) => { - editor.apply(op) - }) - }) - }) - }) - editor.normalize() - editor.onChange() - } catch (err) { - debug('Could not perform undo step', err) - remotePatches.splice(0, remotePatches.length) - Transforms.deselect(editor) - editor.history = {undos: [], redos: []} - SAVING.set(editor, true) - editor.onChange() - return - } - editor.history.redos.push(step) - editor.history.undos.pop() - } - } - } - - editor.redo = () => { - if (readOnly) { - return - } - const {redos} = editor.history - if (redos.length > 0) { - const step = redos[redos.length - 1] - debug('Redoing', step) - if (step.operations.length > 0) { - const otherPatches = remotePatches.filter((item) => item.time >= step.timestamp) - let transformedOperations = step.operations - otherPatches.forEach((item) => { - transformedOperations = flatten( - transformedOperations.map((op) => - transformOperation(editor, item.patch, op, item.snapshot, item.previousSnapshot), - ), - ) - }) - try { - Editor.withoutNormalizing(editor, () => { - withPreserveKeys(editor, () => { - withoutSaving(editor, () => { - // eslint-disable-next-line max-nested-callbacks - transformedOperations.forEach((op) => { - editor.apply(op) - }) - }) - }) - }) - editor.normalize() - editor.onChange() - } catch (err) { - debug('Could not perform redo step', err) - remotePatches.splice(0, remotePatches.length) - Transforms.deselect(editor) - editor.history = {undos: [], redos: []} - SAVING.set(editor, true) - editor.onChange() - return - } - editor.history.undos.push(step) - editor.history.redos.pop() - } - } - } - - // Plugin return - return editor - } -} - -/** - * This will adjust the operation paths and offsets according to the - * remote patches by other editors since the step operations was performed. - */ -function transformOperation( - editor: PortableTextSlateEditor, - patch: Patch, - operation: Operation, - snapshot: PortableTextBlock[] | undefined, - previousSnapshot: PortableTextBlock[] | undefined, -): Operation[] { - if (debugVerbose) { - debug(`Adjusting '${operation.type}' operation paths for '${patch.type}' patch`) - debug(`Operation ${JSON.stringify(operation)}`) - debug(`Patch ${JSON.stringify(patch)}`) - } - - const transformedOperation = {...operation} - - if (patch.type === 'insert' && patch.path.length === 1) { - const insertBlockIndex = (snapshot || []).findIndex((blk) => - isEqual({_key: blk._key}, patch.path[0]), - ) - debug( - `Adjusting block path (+${patch.items.length}) for '${transformedOperation.type}' operation and patch '${patch.type}'`, - ) - return [adjustBlockPath(transformedOperation, patch.items.length, insertBlockIndex)] - } - - if (patch.type === 'unset' && patch.path.length === 1) { - const unsetBlockIndex = (previousSnapshot || []).findIndex((blk) => - isEqual({_key: blk._key}, patch.path[0]), - ) - // If this operation is targeting the same block that got removed, return empty - if ( - 'path' in transformedOperation && - Array.isArray(transformedOperation.path) && - transformedOperation.path[0] === unsetBlockIndex - ) { - debug('Skipping transformation that targeted removed block') - return [] - } - if (debugVerbose) { - debug(`Selection ${JSON.stringify(editor.selection)}`) - debug( - `Adjusting block path (-1) for '${transformedOperation.type}' operation and patch '${patch.type}'`, - ) - } - return [adjustBlockPath(transformedOperation, -1, unsetBlockIndex)] - } - - // Someone reset the whole value - if (patch.type === 'unset' && patch.path.length === 0) { - debug(`Adjusting selection for unset everything patch and ${operation.type} operation`) - return [] - } - - if (patch.type === 'diffMatchPatch') { - const operationTargetBlock = findOperationTargetBlock(editor, transformedOperation) - if (!operationTargetBlock || !isEqual({_key: operationTargetBlock._key}, patch.path[0])) { - return [transformedOperation] - } - const diffPatches = parsePatch(patch.value) - diffPatches.forEach((diffPatch) => { - let adjustOffsetBy = 0 - let changedOffset = diffPatch.utf8Start1 - const {diffs} = diffPatch - diffs.forEach((diff, index) => { - const [diffType, text] = diff - if (diffType === DIFF_INSERT) { - adjustOffsetBy += text.length - changedOffset += text.length - } else if (diffType === DIFF_DELETE) { - adjustOffsetBy -= text.length - changedOffset -= text.length - } else if (diffType === DIFF_EQUAL) { - // Only up to the point where there are no other changes - if (!diffs.slice(index).every(([dType]) => dType === DIFF_EQUAL)) { - changedOffset += text.length - } - } - }) - // Adjust accordingly if someone inserted text in the same node before us - if (transformedOperation.type === 'insert_text') { - if (changedOffset < transformedOperation.offset) { - transformedOperation.offset += adjustOffsetBy - } - } - // Adjust accordingly if someone removed text in the same node before us - if (transformedOperation.type === 'remove_text') { - if (changedOffset <= transformedOperation.offset - transformedOperation.text.length) { - transformedOperation.offset += adjustOffsetBy - } - } - // Adjust set_selection operation's points to new offset - if (transformedOperation.type === 'set_selection') { - const currentFocus = transformedOperation.properties?.focus - ? {...transformedOperation.properties.focus} - : undefined - const currentAnchor = transformedOperation?.properties?.anchor - ? {...transformedOperation.properties.anchor} - : undefined - const newFocus = transformedOperation?.newProperties?.focus - ? {...transformedOperation.newProperties.focus} - : undefined - const newAnchor = transformedOperation?.newProperties?.anchor - ? {...transformedOperation.newProperties.anchor} - : undefined - if ((currentFocus && currentAnchor) || (newFocus && newAnchor)) { - const points = [currentFocus, currentAnchor, newFocus, newAnchor] - points.forEach((point) => { - if (point && changedOffset < point.offset) { - point.offset += adjustOffsetBy - } - }) - if (currentFocus && currentAnchor) { - transformedOperation.properties = { - focus: currentFocus, - anchor: currentAnchor, - } - } - if (newFocus && newAnchor) { - transformedOperation.newProperties = { - focus: newFocus, - anchor: newAnchor, - } - } - } - } - }) - return [transformedOperation] - } - return [transformedOperation] -} -/** - * Adjust the block path for a operation - */ -function adjustBlockPath(operation: Operation, level: number, blockIndex: number): Operation { - const transformedOperation = {...operation} - if ( - blockIndex >= 0 && - transformedOperation.type !== 'set_selection' && - Array.isArray(transformedOperation.path) && - transformedOperation.path[0] >= blockIndex + level && - transformedOperation.path[0] + level > -1 - ) { - const newPath = [transformedOperation.path[0] + level, ...transformedOperation.path.slice(1)] - transformedOperation.path = newPath - } - if (transformedOperation.type === 'set_selection') { - const currentFocus = transformedOperation.properties?.focus - ? {...transformedOperation.properties.focus} - : undefined - const currentAnchor = transformedOperation?.properties?.anchor - ? {...transformedOperation.properties.anchor} - : undefined - const newFocus = transformedOperation?.newProperties?.focus - ? {...transformedOperation.newProperties.focus} - : undefined - const newAnchor = transformedOperation?.newProperties?.anchor - ? {...transformedOperation.newProperties.anchor} - : undefined - if ((currentFocus && currentAnchor) || (newFocus && newAnchor)) { - const points = [currentFocus, currentAnchor, newFocus, newAnchor] - points.forEach((point) => { - if (point && point.path[0] >= blockIndex + level && point.path[0] + level > -1) { - point.path = [point.path[0] + level, ...point.path.slice(1)] - } - }) - if (currentFocus && currentAnchor) { - transformedOperation.properties = { - focus: currentFocus, - anchor: currentAnchor, - } - } - if (newFocus && newAnchor) { - transformedOperation.newProperties = { - focus: newFocus, - anchor: newAnchor, - } - } - } - } - // // Assign fresh point objects (we don't want to mutate the original ones) - return transformedOperation -} - -// Helper functions for editor.apply above - -const shouldMerge = (op: Operation, prev: Operation | undefined): boolean => { - if (op.type === 'set_selection') { - return true - } - - // Text input - if ( - prev && - op.type === 'insert_text' && - prev.type === 'insert_text' && - op.offset === prev.offset + prev.text.length && - Path.equals(op.path, prev.path) && - op.text !== ' ' // Tokenize between words - ) { - return true - } - - // Text deletion - if ( - prev && - op.type === 'remove_text' && - prev.type === 'remove_text' && - op.offset + op.text.length === prev.offset && - Path.equals(op.path, prev.path) - ) { - return true - } - - // Don't merge - return false -} - -const shouldOverwrite = (op: Operation, prev: Operation | undefined): boolean => { - if (prev && op.type === 'set_selection' && prev.type === 'set_selection') { - return true - } - - return false -} - -const shouldClear = (op: Operation): boolean => { - if (op.type === 'set_selection') { - return false - } - - return true -} - -export function withoutSaving(editor: Editor, fn: () => void): void { - const prev = isSaving(editor) - SAVING.set(editor, false) - fn() - SAVING.set(editor, prev) -} - -function createSelectOperation(editor: Editor): SelectionOperation { - return { - type: 'set_selection', - properties: {...editor.selection}, - newProperties: {...editor.selection}, - } -} - -function findOperationTargetBlock( - editor: PortableTextSlateEditor, - operation: Operation, -): Descendant | undefined { - let block: Descendant | undefined - if (operation.type === 'set_selection' && editor.selection) { - block = editor.children[editor.selection.focus.path[0]] - } else if ('path' in operation) { - block = editor.children[operation.path[0]] - } - return block -} diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithUtils.ts b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithUtils.ts deleted file mode 100644 index 4ec76f80c08..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithUtils.ts +++ /dev/null @@ -1,81 +0,0 @@ -import {Editor, Range, Text, Transforms} from 'slate' - -import {type PortableTextMemberSchemaTypes, type PortableTextSlateEditor} from '../../types/editor' -import {debugWithName} from '../../utils/debug' -import {toSlateValue} from '../../utils/values' -import {type PortableTextEditor} from '../PortableTextEditor' - -const debug = debugWithName('plugin:withUtils') - -interface Options { - schemaTypes: PortableTextMemberSchemaTypes - keyGenerator: () => string - portableTextEditor: PortableTextEditor -} -/** - * This plugin makes various util commands available in the editor - * - */ -export function createWithUtils({schemaTypes, keyGenerator, portableTextEditor}: Options) { - return function withUtils(editor: PortableTextSlateEditor): PortableTextSlateEditor { - // Expands the the selection to wrap around the word the focus is at - editor.pteExpandToWord = () => { - const {selection} = editor - if (selection && !Range.isExpanded(selection)) { - const [textNode] = Editor.node(editor, selection.focus, {depth: 2}) - if (!textNode || !Text.isText(textNode) || textNode.text.length === 0) { - debug(`pteExpandToWord: Can't expand to word here`) - return - } - const {focus} = selection - const focusOffset = focus.offset - const charsBefore = textNode.text.slice(0, focusOffset) - const charsAfter = textNode.text.slice(focusOffset, -1) - const isEmpty = (str: string) => str.match(/\s/g) - const whiteSpaceBeforeIndex = charsBefore - .split('') - .reverse() - .findIndex((str) => isEmpty(str)) - const newStartOffset = - whiteSpaceBeforeIndex > -1 ? charsBefore.length - whiteSpaceBeforeIndex : 0 - const whiteSpaceAfterIndex = charsAfter.split('').findIndex((obj) => isEmpty(obj)) - const newEndOffset = - charsBefore.length + - (whiteSpaceAfterIndex > -1 ? whiteSpaceAfterIndex : charsAfter.length + 1) - if (!(newStartOffset === newEndOffset || isNaN(newStartOffset) || isNaN(newEndOffset))) { - debug('pteExpandToWord: Expanding to focused word') - Transforms.setSelection(editor, { - anchor: {...selection.anchor, offset: newStartOffset}, - focus: {...selection.focus, offset: newEndOffset}, - }) - return - } - debug(`pteExpandToWord: Can't expand to word here`) - } - } - - editor.pteCreateEmptyBlock = () => { - const block = toSlateValue( - [ - { - _type: schemaTypes.block.name, - _key: keyGenerator(), - style: schemaTypes.styles[0].value || 'normal', - markDefs: [], - children: [ - { - _type: 'span', - _key: keyGenerator(), - text: '', - marks: [], - }, - ], - }, - ], - portableTextEditor, - )[0] - return block - } - return editor - } -} diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/index.ts b/packages/@sanity/portable-text-editor/src/editor/plugins/index.ts deleted file mode 100644 index 0a52b351899..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/index.ts +++ /dev/null @@ -1,155 +0,0 @@ -import {noop} from 'lodash' -import {type BaseOperation, type Editor, type Node, type NodeEntry} from 'slate' - -import {type PortableTextSlateEditor} from '../../types/editor' -import {type createEditorOptions} from '../../types/options' -import {createOperationToPatches} from '../../utils/operationToPatches' -import {createWithEditableAPI} from './createWithEditableAPI' -import {createWithInsertBreak} from './createWithInsertBreak' -import {createWithMaxBlocks} from './createWithMaxBlocks' -import {createWithObjectKeys} from './createWithObjectKeys' -import {createWithPatches} from './createWithPatches' -import {createWithPlaceholderBlock} from './createWithPlaceholderBlock' -import {createWithPortableTextBlockStyle} from './createWithPortableTextBlockStyle' -import {createWithPortableTextLists} from './createWithPortableTextLists' -import {createWithPortableTextMarkModel} from './createWithPortableTextMarkModel' -import {createWithPortableTextSelections} from './createWithPortableTextSelections' -import {createWithSchemaTypes} from './createWithSchemaTypes' -import {createWithUndoRedo} from './createWithUndoRedo' -import {createWithUtils} from './createWithUtils' - -export {createWithEditableAPI} from './createWithEditableAPI' -export {createWithHotkeys} from './createWithHotKeys' -export {createWithInsertData} from './createWithInsertData' -export {createWithMaxBlocks} from './createWithMaxBlocks' -export {createWithObjectKeys} from './createWithObjectKeys' -export {createWithPatches} from './createWithPatches' -export {createWithPortableTextBlockStyle} from './createWithPortableTextBlockStyle' -export {createWithPortableTextLists} from './createWithPortableTextLists' -export {createWithPortableTextMarkModel} from './createWithPortableTextMarkModel' -export {createWithPortableTextSelections} from './createWithPortableTextSelections' -export {createWithSchemaTypes} from './createWithSchemaTypes' -export {createWithUndoRedo} from './createWithUndoRedo' -export {createWithUtils} from './createWithUtils' - -export interface OriginalEditorFunctions { - apply: (operation: BaseOperation) => void - onChange: () => void - normalizeNode: (entry: NodeEntry) => void -} - -const originalFnMap = new WeakMap() - -export const withPlugins = ( - editor: T, - options: createEditorOptions, -): {editor: PortableTextSlateEditor; subscribe: () => () => void} => { - const e = editor as T & PortableTextSlateEditor - const {keyGenerator, portableTextEditor, patches$, readOnly, maxBlocks} = options - const {schemaTypes, change$} = portableTextEditor - e.subscriptions = [] - if (e.destroy) { - e.destroy() - } else { - // Save a copy of the original editor functions here before they were changed by plugins. - // We will put them back when .destroy is called (see below). - originalFnMap.set(e, { - apply: e.apply, - onChange: e.onChange, - normalizeNode: e.normalizeNode, - }) - } - const operationToPatches = createOperationToPatches(schemaTypes) - const withObjectKeys = createWithObjectKeys(schemaTypes, keyGenerator) - const withSchemaTypes = createWithSchemaTypes({schemaTypes, keyGenerator}) - const withEditableAPI = createWithEditableAPI(portableTextEditor, schemaTypes, keyGenerator) - const withPatches = createWithPatches({ - change$, - keyGenerator, - patches$, - patchFunctions: operationToPatches, - readOnly, - schemaTypes, - }) - const withMaxBlocks = createWithMaxBlocks(maxBlocks || -1) - const withPortableTextLists = createWithPortableTextLists(schemaTypes) - const withUndoRedo = createWithUndoRedo({ - readOnly, - patches$, - blockSchemaType: schemaTypes.block, - }) - const withPortableTextMarkModel = createWithPortableTextMarkModel(schemaTypes, change$) - const withPortableTextBlockStyle = createWithPortableTextBlockStyle(schemaTypes) - - const withPlaceholderBlock = createWithPlaceholderBlock() - - const withInsertBreak = createWithInsertBreak(schemaTypes) - - const withUtils = createWithUtils({keyGenerator, schemaTypes, portableTextEditor}) - const withPortableTextSelections = createWithPortableTextSelections(change$, schemaTypes) - - e.destroy = () => { - const originalFunctions = originalFnMap.get(e) - if (!originalFunctions) { - throw new Error('Could not find pristine versions of editor functions') - } - e.apply = originalFunctions.apply - e.history = {undos: [], redos: []} - e.normalizeNode = originalFunctions.normalizeNode - e.onChange = originalFunctions.onChange - } - if (readOnly) { - return { - editor: withSchemaTypes( - withObjectKeys( - withPortableTextMarkModel( - withPortableTextBlockStyle( - withUtils( - withPlaceholderBlock( - withPortableTextLists( - withPortableTextSelections(withEditableAPI(withInsertBreak(e))), - ), - ), - ), - ), - ), - ), - ), - subscribe: () => noop, - } - } - - // Ordering is important here, selection dealing last, data manipulation in the middle and core model stuff first. - return { - editor: withSchemaTypes( - withObjectKeys( - withPortableTextMarkModel( - withPortableTextBlockStyle( - withPortableTextLists( - withPlaceholderBlock( - withUtils( - withMaxBlocks( - withUndoRedo( - withPatches(withPortableTextSelections(withEditableAPI(withInsertBreak(e)))), - ), - ), - ), - ), - ), - ), - ), - ), - ), - subscribe: () => { - const unsubscribes: (() => void)[] = [] - editor.subscriptions.forEach((subscribeFn) => { - unsubscribes.push(subscribeFn()) - }) - return () => { - unsubscribes.forEach((unsubscribeFn) => { - unsubscribeFn() - }) - } - }, - } -} diff --git a/packages/@sanity/portable-text-editor/src/index.ts b/packages/@sanity/portable-text-editor/src/index.ts deleted file mode 100644 index c2711eb712f..00000000000 --- a/packages/@sanity/portable-text-editor/src/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export type {PortableTextEditableProps} from './editor/Editable' -export {PortableTextEditable} from './editor/Editable' -export {usePortableTextEditor} from './editor/hooks/usePortableTextEditor' -export {defaultKeyGenerator as keyGenerator} from './editor/hooks/usePortableTextEditorKeyGenerator' -export {usePortableTextEditorSelection} from './editor/hooks/usePortableTextEditorSelection' -export type {PortableTextEditorProps} from './editor/PortableTextEditor' -export {PortableTextEditor} from './editor/PortableTextEditor' -export * from './types/editor' -export * from './types/options' -export * from './types/patch' -export {compactPatches} from './utils/patches' diff --git a/packages/@sanity/portable-text-editor/src/patch/PatchEvent.ts b/packages/@sanity/portable-text-editor/src/patch/PatchEvent.ts deleted file mode 100644 index 73fd66af881..00000000000 --- a/packages/@sanity/portable-text-editor/src/patch/PatchEvent.ts +++ /dev/null @@ -1,33 +0,0 @@ -import {type PathSegment} from '@sanity/types' -import {flatten} from 'lodash' - -import {type Patch} from '../types/patch' -import {diffMatchPatch, insert, prefixPath, set, setIfMissing, unset} from './patches' - -type PatchArg = Patch | Array - -export default class PatchEvent { - static from(...patches: Array) { - return new PatchEvent(flatten(patches)) - } - - patches: Array - - constructor(patches: Array) { - this.patches = patches - } - - prepend(...patches: Array): PatchEvent { - return PatchEvent.from([...flatten(patches), ...this.patches]) - } - - append(...patches: Array): PatchEvent { - return PatchEvent.from([...this.patches, ...flatten(patches)]) - } - - prefixAll(segment: PathSegment): PatchEvent { - return PatchEvent.from(this.patches.map((patch) => prefixPath(patch, segment))) - } -} - -export {diffMatchPatch, insert, PatchEvent, set, setIfMissing, unset} diff --git a/packages/@sanity/portable-text-editor/src/patch/applyPatch.ts b/packages/@sanity/portable-text-editor/src/patch/applyPatch.ts deleted file mode 100644 index b25661a6d47..00000000000 --- a/packages/@sanity/portable-text-editor/src/patch/applyPatch.ts +++ /dev/null @@ -1,29 +0,0 @@ -import {isObject, isString} from 'lodash' - -import applyArrayPatch from './array' -import applyObjectPatch from './object' -import applyPrimitivePatch from './primitive' -import applyStringPatch from './string' - -export function applyAll(value: any, patches: any[]) { - return patches.reduce(_apply, value) -} - -function applyPatch(value: string, patch: {type: string; path: any[]; value: any}) { - if (Array.isArray(value)) { - return applyArrayPatch(value, patch as any) - } - if (isString(value)) { - return applyStringPatch(value, patch) - } - if (isObject(value)) { - return applyObjectPatch(value, patch) - } - return applyPrimitivePatch(value, patch) -} - -export default function _apply(value: string, patch: {type: string; path: any[]; value: any}) { - const res = applyPatch(value, patch) - // console.log('applyPatch(%o, %o) : %o (noop? %o)', value, patch, res, value === res) - return res -} diff --git a/packages/@sanity/portable-text-editor/src/patch/array.ts b/packages/@sanity/portable-text-editor/src/patch/array.ts deleted file mode 100644 index 96b5f15d5ac..00000000000 --- a/packages/@sanity/portable-text-editor/src/patch/array.ts +++ /dev/null @@ -1,89 +0,0 @@ -import {type PathSegment} from '@sanity/types' -import {findIndex} from 'lodash' - -import applyPatch from './applyPatch' -import insert from './arrayInsert' - -const hasOwn = Object.prototype.hasOwnProperty.call.bind(Object.prototype.hasOwnProperty) - -function move(arr: any[], from: number, to: any) { - const nextValue = arr.slice() - const val = nextValue[from] - nextValue.splice(from, 1) - nextValue.splice(to, 0, val) - return nextValue -} - -function findTargetIndex(array: any[], pathSegment: PathSegment) { - if (typeof pathSegment === 'number') { - return pathSegment - } - const index = findIndex(array, pathSegment) - return index === -1 ? false : index -} - -export default function apply( - value: any, - patch: {type: any; path: any; value: any; position: any; items: any}, -) { - const nextValue = value.slice() // make a copy for internal mutation - - if (patch.path.length === 0) { - // its directed to me - if (patch.type === 'setIfMissing') { - if (!Array.isArray(patch.value)) { - // eslint-disable-line max-depth - throw new Error('Cannot set value of an array to a non-array') - } - return value === undefined ? patch.value : value - } else if (patch.type === 'set') { - if (!Array.isArray(patch.value)) { - // eslint-disable-line max-depth - throw new Error('Cannot set value of an array to a non-array') - } - return patch.value - } else if (patch.type === 'unset') { - return undefined - } else if (patch.type === 'move') { - if (!patch.value || !hasOwn(patch.value, 'from') || !hasOwn(patch.value, 'to')) { - // eslint-disable-line max-depth - throw new Error( - `Invalid value of 'move' patch. Expected a value with "from" and "to" indexes, instead got: ${JSON.stringify( - patch.value, - )}`, - ) - } - return move(nextValue, patch.value.from, patch.value.to) - } - throw new Error(`Invalid array operation: ${patch.type}`) - } - - const [head, ...tail] = patch.path - - const index = findTargetIndex(value, head) - - // If the given selector could not be found, return as-is - if (index === false) { - return nextValue - } - - if (tail.length === 0) { - if (patch.type === 'insert') { - const {position, items} = patch - return insert(value, position, index, items) - } else if (patch.type === 'unset') { - if (typeof index !== 'number') { - throw new Error(`Expected array index to be a number, instead got "${index}"`) - } - nextValue.splice(index, 1) - return nextValue - } - } - - // The patch is not directed to me - nextValue[index] = applyPatch(nextValue[index], { - ...patch, - path: tail, - }) - return nextValue -} diff --git a/packages/@sanity/portable-text-editor/src/patch/arrayInsert.ts b/packages/@sanity/portable-text-editor/src/patch/arrayInsert.ts deleted file mode 100644 index 04406fafd19..00000000000 --- a/packages/@sanity/portable-text-editor/src/patch/arrayInsert.ts +++ /dev/null @@ -1,27 +0,0 @@ -export const BEFORE = 'before' -export const AFTER = 'after' - -export default function insert(array: any[], position: string, index: number, ...args: any[]) { - if (position !== BEFORE && position !== AFTER) { - throw new Error(`Invalid position "${position}", must be either ${BEFORE} or ${AFTER}`) - } - - const items = flatten(...args) - - if (array.length === 0) { - return items - } - - const len = array.length - const idx = Math.abs((len + index) % len) % len - - const normalizedIdx = position === 'after' ? idx + 1 : idx - - const copy = array.slice() - copy.splice(normalizedIdx, 0, ...flatten(items)) - return copy -} - -function flatten(...values: any[]) { - return values.reduce((prev, item) => prev.concat(item), []) -} diff --git a/packages/@sanity/portable-text-editor/src/patch/object.ts b/packages/@sanity/portable-text-editor/src/patch/object.ts deleted file mode 100644 index f0b8813c227..00000000000 --- a/packages/@sanity/portable-text-editor/src/patch/object.ts +++ /dev/null @@ -1,39 +0,0 @@ -import {clone, isObject, omit} from 'lodash' - -import applyPatch from './applyPatch' - -export default function apply(value: any, patch: {type: any; path: any; value: any}) { - const nextValue = clone(value) - if (patch.path.length === 0) { - // its directed to me - if (patch.type === 'set') { - if (!isObject(patch.value)) { - // eslint-disable-line max-depth - throw new Error('Cannot set value of an object to a non-object') - } - return patch.value - } else if (patch.type === 'unset') { - return undefined - } else if (patch.type === 'setIfMissing') { - // console.log('IS IT missing?', value) - return value === undefined ? patch.value : value - } - throw new Error(`Invalid object operation: ${patch.type}`) - } - - // The patch is not directed to me - const [head, ...tail] = patch.path - if (typeof head !== 'string') { - throw new Error(`Expected field name to be a string, instad got: ${head}`) - } - - if (tail.length === 0 && patch.type === 'unset') { - return omit(nextValue, head) - } - - nextValue[head] = applyPatch(nextValue[head], { - ...patch, - path: tail, - }) - return nextValue -} diff --git a/packages/@sanity/portable-text-editor/src/patch/patches.ts b/packages/@sanity/portable-text-editor/src/patch/patches.ts deleted file mode 100644 index 367b4b69fdb..00000000000 --- a/packages/@sanity/portable-text-editor/src/patch/patches.ts +++ /dev/null @@ -1,53 +0,0 @@ -import {makePatches, stringifyPatches} from '@sanity/diff-match-patch' -import {type Path, type PathSegment} from '@sanity/types' - -import { - type DiffMatchPatch, - type InsertPatch, - type InsertPosition, - type SetIfMissingPatch, - type SetPatch, - type UnsetPatch, -} from '../types/patch' - -export function setIfMissing(value: any, path: Path = []): SetIfMissingPatch { - return { - type: 'setIfMissing', - path, - value, - } -} - -export function diffMatchPatch( - currentValue: string, - nextValue: string, - path: Path = [], -): DiffMatchPatch { - const patches = makePatches(currentValue, nextValue) - const patch = stringifyPatches(patches) - return {type: 'diffMatchPatch', path, value: patch} -} - -export function insert(items: any[], position: InsertPosition, path: Path = []): InsertPatch { - return { - type: 'insert', - path, - position, - items, - } -} - -export function set(value: any, path: Path = []): SetPatch { - return {type: 'set', path, value} -} - -export function unset(path: Path = []): UnsetPatch { - return {type: 'unset', path} -} - -export function prefixPath(patch: T, segment: PathSegment): T { - return { - ...patch, - path: [segment, ...patch.path], - } -} diff --git a/packages/@sanity/portable-text-editor/src/patch/primitive.ts b/packages/@sanity/portable-text-editor/src/patch/primitive.ts deleted file mode 100644 index a4b2362ca2f..00000000000 --- a/packages/@sanity/portable-text-editor/src/patch/primitive.ts +++ /dev/null @@ -1,43 +0,0 @@ -const OPERATIONS: Record = { - replace(_currentValue: any, nextValue: any) { - return nextValue - }, - set(_currentValue: any, nextValue: any) { - return nextValue - }, - setIfMissing(currentValue: any, nextValue: any) { - return currentValue === undefined ? nextValue : currentValue - }, - unset(_currentValue: any, _nextValue: any) { - return undefined - }, - inc(currentValue: any, nextValue: any) { - return currentValue + nextValue - }, - dec(currentValue: any, nextValue: any) { - return currentValue - nextValue - }, -} - -const SUPPORTED_PATCH_TYPES = Object.keys(OPERATIONS) - -export default function apply(value: any, patch: any) { - if (!SUPPORTED_PATCH_TYPES.includes(patch.type)) { - throw new Error( - `Received patch of unsupported type: "${JSON.stringify( - patch.type, - )}" for primitives. This is most likely a bug.`, - ) - } - - if (patch.path.length > 0) { - throw new Error( - `Cannot apply deep operations on primitive values. Received patch with type "${ - patch.type - }" and path "${patch.path - .map((path: any) => JSON.stringify(path)) - .join('.')} that targeted the value "${JSON.stringify(value)}"`, - ) - } - return OPERATIONS[patch.type](value, patch.value) -} diff --git a/packages/@sanity/portable-text-editor/src/patch/string.ts b/packages/@sanity/portable-text-editor/src/patch/string.ts deleted file mode 100644 index b140d9c3e40..00000000000 --- a/packages/@sanity/portable-text-editor/src/patch/string.ts +++ /dev/null @@ -1,51 +0,0 @@ -import {applyPatches, parsePatch} from '@sanity/diff-match-patch' - -type fn = (oldVal: any, newVal: any) => any -const OPERATIONS: Record = { - replace(currentValue: any, nextValue: any) { - return nextValue - }, - set(currentValue: any, nextValue: any) { - return nextValue - }, - setIfMissing(currentValue: undefined, nextValue: any) { - return currentValue === undefined ? nextValue : currentValue - }, - unset(currentValue: any, nextValue: any) { - return undefined - }, - diffMatchPatch(currentValue: string, nextValue: string): string { - const [result] = applyPatches(parsePatch(nextValue), currentValue, { - allowExceedingIndices: true, - }) - return result - }, -} - -const SUPPORTED_PATCH_TYPES = Object.keys(OPERATIONS) - -export default function apply( - value: string, - patch: {type: string; path: any[]; value: any}, -): string { - if (!SUPPORTED_PATCH_TYPES.includes(patch.type)) { - throw new Error( - `Received patch of unsupported type: "${JSON.stringify( - patch.type, - )}" for string. This is most likely a bug.`, - ) - } - - if (patch.path.length > 0) { - throw new Error( - `Cannot apply deep operations on string values. Received patch with type "${ - patch.type - }" and path "${patch.path.join('.')} that targeted the value "${JSON.stringify(value)}"`, - ) - } - const func = OPERATIONS[patch.type] - if (func) { - return func(value, patch.value) - } - throw new Error('Unknown patch type') -} diff --git a/packages/@sanity/portable-text-editor/src/types/editor.ts b/packages/@sanity/portable-text-editor/src/types/editor.ts deleted file mode 100644 index 5faccf67ee5..00000000000 --- a/packages/@sanity/portable-text-editor/src/types/editor.ts +++ /dev/null @@ -1,576 +0,0 @@ -import { - type ArraySchemaType, - type BlockDecoratorDefinition, - type BlockListDefinition, - type BlockSchemaType, - type BlockStyleDefinition, - type ObjectSchemaType, - type Path, - type PortableTextBlock, - type PortableTextChild, - type PortableTextListBlock, - type PortableTextObject, - type PortableTextSpan, - type PortableTextTextBlock, - type SpanSchemaType, - type TypedObject, -} from '@sanity/types' -import { - type ClipboardEvent, - type FocusEvent, - type KeyboardEvent, - type PropsWithChildren, - type ReactElement, - type RefObject, -} from 'react' -import {type Observable, type Subject} from 'rxjs' -import {type Descendant, type Node as SlateNode, type Operation as SlateOperation} from 'slate' -import {type ReactEditor} from 'slate-react' -import {type DOMNode} from 'slate-react/dist/utils/dom' - -import {type PortableTextEditableProps} from '../editor/Editable' -import {type PortableTextEditor} from '../editor/PortableTextEditor' -import {type Patch} from '../types/patch' - -/** @beta */ -export interface EditableAPIDeleteOptions { - mode?: 'blocks' | 'children' | 'selected' -} - -/** @beta */ -export interface EditableAPI { - activeAnnotations: () => PortableTextObject[] - isAnnotationActive: (annotationType: PortableTextObject['_type']) => boolean - addAnnotation: ( - type: ObjectSchemaType, - value?: {[prop: string]: unknown}, - ) => {spanPath: Path; markDefPath: Path} | undefined - blur: () => void - delete: (selection: EditorSelection, options?: EditableAPIDeleteOptions) => void - findByPath: (path: Path) => [PortableTextBlock | PortableTextChild | undefined, Path | undefined] - findDOMNode: (element: PortableTextBlock | PortableTextChild) => DOMNode | undefined - focus: () => void - focusBlock: () => PortableTextBlock | undefined - focusChild: () => PortableTextChild | undefined - getSelection: () => EditorSelection - getFragment: () => PortableTextBlock[] | undefined - getValue: () => PortableTextBlock[] | undefined - hasBlockStyle: (style: string) => boolean - hasListStyle: (listStyle: string) => boolean - insertBlock: (type: BlockSchemaType | ObjectSchemaType, value?: {[prop: string]: unknown}) => Path - insertChild: (type: SpanSchemaType | ObjectSchemaType, value?: {[prop: string]: unknown}) => Path - insertBreak: () => void - isCollapsedSelection: () => boolean - isExpandedSelection: () => boolean - isMarkActive: (mark: string) => boolean - isSelectionsOverlapping: (selectionA: EditorSelection, selectionB: EditorSelection) => boolean - isVoid: (element: PortableTextBlock | PortableTextChild) => boolean - marks: () => string[] - redo: () => void - removeAnnotation: (type: ObjectSchemaType) => void - select: (selection: EditorSelection) => void - toggleBlockStyle: (blockStyle: string) => void - toggleList: (listStyle: string) => void - toggleMark: (mark: string) => void - undo: () => void -} - -/** @internal */ -export type EditorNode = SlateNode & { - _key: string - _type: string -} -/** @internal */ -export type HistoryItem = { - operations: SlateOperation[] - timestamp: Date -} -/** @internal */ -export interface History { - redos: HistoryItem[] - undos: HistoryItem[] -} - -/** @beta */ -export type EditorSelectionPoint = {path: Path; offset: number} -/** @beta */ -export type EditorSelection = { - anchor: EditorSelectionPoint - focus: EditorSelectionPoint - backward?: boolean -} | null -/** @internal */ -export interface PortableTextSlateEditor extends ReactEditor { - _key: 'editor' - _type: 'editor' - destroy: () => void - createPlaceholderBlock: () => Descendant - editable: EditableAPI - history: History - insertPortableTextData: (data: DataTransfer) => boolean - insertTextOrHTMLData: (data: DataTransfer) => boolean - isTextBlock: (value: unknown) => value is PortableTextTextBlock - isTextSpan: (value: unknown) => value is PortableTextSpan - isListBlock: (value: unknown) => value is PortableTextListBlock - subscriptions: (() => () => void)[] - - /** - * Increments selected list items levels, or decrements them if `reverse` is true. - * - * @param reverse - if true, decrement instead of incrementing - * @returns True if anything was incremented in the selection - */ - pteIncrementBlockLevels: (reverse?: boolean) => boolean - - /** - * Toggle selected blocks as listItem - * - * @param listStyle - Style of list item to toggle on/off - */ - pteToggleListItem: (listStyle: string) => void - - /** - * Set selected block as listItem - * - * @param listStyle - Style of list item to set - */ - pteSetListItem: (listStyle: string) => void - - /** - * Unset selected block as listItem - * - * @param listStyle - Style of list item to unset - */ - pteUnsetListItem: (listStyle: string) => void - - /** - * Ends a list - * - * @returns True if a list was ended in the selection - */ - pteEndList: () => boolean - - /** - * Toggle marks in the selection - * - * @param mark - Mark to toggle on/off - */ - pteToggleMark: (mark: string) => void - - /** - * Test if a mark is active in the current selection - * - * @param mark - Mark to check whether or not is active - */ - pteIsMarkActive: (mark: string) => boolean - - /** - * Toggle the selected block style - * - * @param style - The style name - * - */ - pteToggleBlockStyle: (style: string) => void - - /** - * Test if the current selection has a certain block style - * - * @param style - The style name - * - */ - pteHasBlockStyle: (style: string) => boolean - - /** - * Test if the current selection has a certain list style - * - * @param listStyle - Style name to check whether or not the selection has - * - */ - pteHasListStyle: (style: string) => boolean - - /** - * Try to expand the current selection to a word - */ - pteExpandToWord: () => void - - /** - * Use hotkeys - */ - pteWithHotKeys: (event: KeyboardEvent) => void - - /** - * Helper function that creates an empty text block - */ - pteCreateEmptyBlock: () => Descendant - - /** - * Undo - */ - undo: () => void - - /** - * Redo - */ - redo: () => void -} - -/** - * The editor has mutated it's content. - * @beta */ -export type MutationChange = { - type: 'mutation' - patches: Patch[] - snapshot: PortableTextBlock[] | undefined -} - -/** - * The editor has produced a patch - * @beta */ -export type PatchChange = { - type: 'patch' - patch: Patch -} - -/** - * The editor has received a new (props) value - * @beta */ -export type ValueChange = { - type: 'value' - value: PortableTextBlock[] | undefined -} - -/** - * The editor has a new selection - * @beta */ -export type SelectionChange = { - type: 'selection' - selection: EditorSelection -} - -/** - * The editor received focus - * @beta */ -export type FocusChange = { - type: 'focus' - event: FocusEvent -} - -/** @beta */ -export type UnsetChange = { - type: 'unset' - previousValue: PortableTextBlock[] -} - -/** - * The editor blurred - * @beta */ -export type BlurChange = { - type: 'blur' - event: FocusEvent -} - -/** - * The editor is currently loading something - * Could be used to show a spinner etc. - * @beta */ -export type LoadingChange = { - type: 'loading' - isLoading: boolean -} - -/** - * The editor content is ready to be edited by the user - * @beta */ -export type ReadyChange = { - type: 'ready' -} - -/** - * The editor produced an error - * @beta */ -export type ErrorChange = { - type: 'error' - name: string // short computer readable name - level: 'warning' | 'error' - description: string - data?: unknown -} - -/** - * The editor has invalid data in the value that can be resolved by the user - * @beta */ -export type InvalidValueResolution = { - autoResolve?: boolean - patches: Patch[] - description: string - action: string - item: PortableTextBlock[] | PortableTextBlock | PortableTextChild | undefined - - /** - * i18n keys for the description and action - * - * These are in addition to the description and action properties, to decouple the editor from - * the i18n system, and allow usage without it. The i18n keys take precedence over the - * description and action properties, if i18n framework is available. - */ - i18n: { - description: `inputs.portable-text.invalid-value.${Lowercase}.description` - action: `inputs.portable-text.invalid-value.${Lowercase}.action` - values?: Record - } -} - -/** - * The editor has an invalid value - * @beta */ -export type InvalidValue = { - type: 'invalidValue' - resolution: InvalidValueResolution | null - value: PortableTextBlock[] | undefined -} - -/** - * The editor performed a undo history step - * @beta */ -export type UndoChange = { - type: 'undo' - patches: Patch[] - timestamp: Date -} - -/** - * The editor performed redo history step - * @beta */ -export type RedoChange = { - type: 'redo' - patches: Patch[] - timestamp: Date -} - -/** - * The editor was either connected or disconnected to the network - * To show out of sync warnings etc when in collaborative mode. - * @beta */ -export type ConnectionChange = { - type: 'connection' - value: 'online' | 'offline' -} - -/** - * When the editor changes, it will emit a change item describing the change - * @beta */ -export type EditorChange = - | BlurChange - | ConnectionChange - | ErrorChange - | FocusChange - | InvalidValue - | LoadingChange - | MutationChange - | PatchChange - | ReadyChange - | RedoChange - | SelectionChange - | UndoChange - | UnsetChange - | ValueChange - -export type EditorChanges = Subject - -/** @beta */ -export type OnPasteResult = - | { - insert?: TypedObject[] - path?: Path - } - | undefined -export type OnPasteResultOrPromise = OnPasteResult | Promise - -/** @beta */ -export interface PasteData { - event: ClipboardEvent - path: Path - schemaTypes: PortableTextMemberSchemaTypes - value: PortableTextBlock[] | undefined -} - -/** @beta */ -export type OnPasteFn = (data: PasteData) => OnPasteResultOrPromise - -/** @beta */ -export type OnBeforeInputFn = (event: InputEvent) => void - -/** @beta */ -export type OnCopyFn = ( - event: ClipboardEvent, -) => undefined | unknown - -/** @beta */ -export type PatchObservable = Observable<{ - patches: Patch[] - snapshot: PortableTextBlock[] | undefined -}> - -/** @beta */ -export interface BlockRenderProps { - children: ReactElement - editorElementRef: RefObject - focused: boolean - level?: number - listItem?: string - path: Path - selected: boolean - style?: string - schemaType: ObjectSchemaType - /** @deprecated Use `schemaType` instead */ - type: ObjectSchemaType - value: PortableTextBlock -} - -/** @beta */ -export interface BlockChildRenderProps { - annotations: PortableTextObject[] - children: ReactElement - editorElementRef: RefObject - focused: boolean - path: Path - selected: boolean - schemaType: ObjectSchemaType - /** @deprecated Use `schemaType` instead */ - type: ObjectSchemaType - value: PortableTextChild -} - -/** @beta */ -export interface BlockAnnotationRenderProps { - block: PortableTextBlock - children: ReactElement - editorElementRef: RefObject - focused: boolean - path: Path - schemaType: ObjectSchemaType - selected: boolean - /** @deprecated Use `schemaType` instead */ - type: ObjectSchemaType - value: PortableTextObject -} -/** @beta */ -export interface BlockDecoratorRenderProps { - children: ReactElement - editorElementRef: RefObject - focused: boolean - path: Path - schemaType: BlockDecoratorDefinition - selected: boolean - /** @deprecated Use `schemaType` instead */ - type: BlockDecoratorDefinition - value: string -} -/** @beta */ - -export interface BlockListItemRenderProps { - block: PortableTextTextBlock - children: ReactElement - editorElementRef: RefObject - focused: boolean - level: number - path: Path - schemaType: BlockListDefinition - selected: boolean - value: string -} - -/** @beta */ -export type RenderBlockFunction = (props: BlockRenderProps) => JSX.Element - -/** @beta */ -export type RenderChildFunction = (props: BlockChildRenderProps) => JSX.Element - -/** @beta */ -export type RenderEditableFunction = (props: PortableTextEditableProps) => JSX.Element - -/** @beta */ -export type RenderAnnotationFunction = (props: BlockAnnotationRenderProps) => JSX.Element - -/** @beta */ -export type RenderStyleFunction = (props: BlockStyleRenderProps) => JSX.Element - -/** @beta */ - -export interface BlockStyleRenderProps { - block: PortableTextTextBlock - children: ReactElement - editorElementRef: RefObject - focused: boolean - path: Path - selected: boolean - schemaType: BlockStyleDefinition - value: string -} - -/** @beta */ -export type RenderListItemFunction = (props: BlockListItemRenderProps) => JSX.Element - -/** @beta */ -export type RenderDecoratorFunction = (props: BlockDecoratorRenderProps) => JSX.Element - -/** @beta */ -export type ScrollSelectionIntoViewFunction = ( - editor: PortableTextEditor, - domRange: globalThis.Range, -) => void - -/** - * Parameters for the callback that will be called for a RangeDecoration's onMoved. - * @alpha */ -export interface RangeDecorationOnMovedDetails { - rangeDecoration: RangeDecoration - newSelection: EditorSelection - origin: 'remote' | 'local' -} -/** - * A range decoration is a UI affordance that wraps a given selection range in the editor - * with a custom component. This can be used to highlight search results, - * mark validation errors on specific words, draw user presence and similar. - * @alpha */ -export interface RangeDecoration { - /** - * A component for rendering the range decoration. - * The component will receive the children (text) of the range decoration as its children. - * - * @example - * ```ts - * (rangeComponentProps: PropsWithChildren) => ( - * - * {rangeComponentProps.children} - * - * ) - * ``` - */ - component: (props: PropsWithChildren) => ReactElement - /** - * The editor content selection range - */ - selection: EditorSelection - /** - * A optional callback that will be called when the range decoration potentially moves according to user edits. - */ - onMoved?: (details: RangeDecorationOnMovedDetails) => void - /** - * A custom payload that can be set on the range decoration - */ - payload?: Record -} - -/** @internal */ -export type PortableTextMemberSchemaTypes = { - annotations: (ObjectSchemaType & {i18nTitleKey?: string})[] - block: ObjectSchemaType - blockObjects: ObjectSchemaType[] - decorators: BlockDecoratorDefinition[] - inlineObjects: ObjectSchemaType[] - portableText: ArraySchemaType - span: ObjectSchemaType - styles: BlockStyleDefinition[] - lists: BlockListDefinition[] -} diff --git a/packages/@sanity/portable-text-editor/src/types/options.ts b/packages/@sanity/portable-text-editor/src/types/options.ts deleted file mode 100644 index b866631b8e3..00000000000 --- a/packages/@sanity/portable-text-editor/src/types/options.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {type BaseSyntheticEvent} from 'react' - -import {type PortableTextEditor} from '../editor/PortableTextEditor' -import {type PatchObservable} from './editor' - -export type createEditorOptions = { - keyGenerator: () => string - patches$?: PatchObservable - portableTextEditor: PortableTextEditor - readOnly: boolean - maxBlocks?: number -} - -export type HotkeyOptions = { - marks?: Record - custom?: Record void> -} diff --git a/packages/@sanity/portable-text-editor/src/types/patch.ts b/packages/@sanity/portable-text-editor/src/types/patch.ts deleted file mode 100644 index ca2231fae3c..00000000000 --- a/packages/@sanity/portable-text-editor/src/types/patch.ts +++ /dev/null @@ -1,65 +0,0 @@ -import {type Path} from '@sanity/types' - -export type JSONValue = number | string | boolean | {[key: string]: JSONValue} | JSONValue[] - -export type Origin = 'remote' | 'local' | 'internal' - -export type IncPatch = { - path: Path - origin?: Origin - type: 'inc' - value: JSONValue -} - -export type DecPatch = { - path: Path - origin?: Origin - type: 'dec' - value: JSONValue -} - -export type SetPatch = { - path: Path - type: 'set' - origin?: Origin - value: JSONValue -} - -export type SetIfMissingPatch = { - path: Path - origin?: Origin - type: 'setIfMissing' - value: JSONValue -} - -export type UnsetPatch = { - path: Path - origin?: Origin - type: 'unset' -} - -export type InsertPosition = 'before' | 'after' | 'replace' - -export type InsertPatch = { - path: Path - origin?: Origin - type: 'insert' - position: InsertPosition - items: JSONValue[] -} - -export type DiffMatchPatch = { - path: Path - type: 'diffMatchPatch' - origin?: Origin - value: string -} - -export type Patch = - | SetPatch - | SetIfMissingPatch - | UnsetPatch - | InsertPatch - | DiffMatchPatch - | IncPatch - | DecPatch diff --git a/packages/@sanity/portable-text-editor/src/types/slate.ts b/packages/@sanity/portable-text-editor/src/types/slate.ts deleted file mode 100644 index 44e2b1395ad..00000000000 --- a/packages/@sanity/portable-text-editor/src/types/slate.ts +++ /dev/null @@ -1,25 +0,0 @@ -import {type PortableTextSpan, type PortableTextTextBlock} from '@sanity/types' -import {type BaseEditor, type Descendant} from 'slate' -import {type ReactEditor} from 'slate-react' - -import {type PortableTextSlateEditor} from '..' - -export interface VoidElement { - _type: string - _key: string - children: Descendant[] - __inline: boolean - value: Record -} - -export interface SlateTextBlock extends Omit { - children: Descendant[] -} - -declare module 'slate' { - interface CustomTypes { - Editor: BaseEditor & ReactEditor & PortableTextSlateEditor - Element: SlateTextBlock | VoidElement - Text: PortableTextSpan - } -} diff --git a/packages/@sanity/portable-text-editor/src/utils/__tests__/dmpToOperations.test.ts b/packages/@sanity/portable-text-editor/src/utils/__tests__/dmpToOperations.test.ts deleted file mode 100644 index 421871a8604..00000000000 --- a/packages/@sanity/portable-text-editor/src/utils/__tests__/dmpToOperations.test.ts +++ /dev/null @@ -1,181 +0,0 @@ -import {describe, expect, test} from '@jest/globals' -import {makeDiff, makePatches, stringifyPatches} from '@sanity/diff-match-patch' -import { - isPortableTextSpan, - isPortableTextTextBlock, - type Path, - type PortableTextBlock, - type PortableTextSpan, - type PortableTextTextBlock, -} from '@sanity/types' -import {type Descendant, type Operation} from 'slate' - -import {type PortableTextSlateEditor} from '../../types/editor' -import {type DiffMatchPatch} from '../../types/patch' -import {diffMatchPatch} from '../applyPatch' - -describe('operationToPatches: diffMatchPatch', () => { - test.todo('skips patches for blocks that cannot be found locally') - test.todo('skips patches for non-PT-blocks') - test.todo('skips patches for non-spans') - test.todo('throws if cannot find span') - - test('should apply the most basic additive operation correctly', () => { - const source = 'Hello' - const target = 'Hello there' - const patch = getPteDmpPatch(stringifyPatches(makePatches(makeDiff(source, target)))) - const editor = getMockEditor({text: source}) - expect(diffMatchPatch(editor, patch)).toBe(true) - expect(editor.getText()).toBe(target) - }) - - test('should apply the most basic removal operation correctly', () => { - const source = 'Hello there' - const target = 'Hello' - const patch = getPteDmpPatch(stringifyPatches(makePatches(makeDiff(source, target)))) - const editor = getMockEditor({text: source}) - expect(diffMatchPatch(editor, patch)).toBe(true) - expect(editor.getText()).toBe(target) - }) - - test('should treat equality as noops', () => { - const source = 'Hello' - const target = 'Hello' - const patch = getPteDmpPatch(stringifyPatches(makePatches(makeDiff(source, target)))) - const editor = getMockEditor({text: source}) - expect(diffMatchPatch(editor, patch)).toBe(true) - expect(editor.getText()).toBe(target) - }) - - test('should apply combined add + remove operations', () => { - const source = 'A quick brown fox jumps over the very lazy dog' - const target = 'The quick brown fox jumps over the lazy dog' - const patch = getPteDmpPatch(stringifyPatches(makePatches(makeDiff(source, target)))) - const editor = getMockEditor({text: source}) - expect(diffMatchPatch(editor, patch)).toBe(true) - expect(editor.getText()).toBe(target) - }) - - test('should apply combined add + remove operations', () => { - const source = 'Many quick brown fox jumps over the very lazy dog' - const target = 'The many, quick, brown, foxes jumps over all of the lazy dogs' - const patch = getPteDmpPatch(stringifyPatches(makePatches(makeDiff(source, target)))) - const editor = getMockEditor({text: source}) - expect(diffMatchPatch(editor, patch)).toBe(true) - expect(editor.getText()).toBe(target) - }) - - test('should apply reverse line edits correctly', () => { - const line1 = 'The quick brown fox jumps over the lazy dog' - const line2 = 'But the slow green frog jumps over the wild cat' - const source = [line1, line2, line1, line2].join('\n') - const target = [line2, line1, line2, line1].join('\n') - const patch = getPteDmpPatch(stringifyPatches(makePatches(makeDiff(source, target)))) - const editor = getMockEditor({text: source}) - expect(diffMatchPatch(editor, patch)).toBe(true) - expect(editor.getText()).toBe(target) - }) - - test('should apply larger text differences correctly', () => { - const source = `Portable Text is a agnostic abstraction of "rich text" that can be stringified into any markup language, for instance HTML, Markdown, SSML, XML, etc. It's designed to be efficient for collaboration, and makes it possible to enrich rich text with data structures in depth.\n\nPortable Text is built on the idea of rich text as an array of blocks, themselves arrays of children spans. Each block can have a style and a set of mark dfinitions, which describe data structures distributed on the children spans. Portable Text also allows for inserting arbitrary data objects in the array, only requiring _type-key. Portable Text also allows for custom objects in the root array, enabling rendering environments to mix rich text with custom content types.\n\nPortable Text is a combination of arrays and objects. In its simplest form it's an array of objects with an array of children. Some definitions: \n- Block: Typically recognized as a section of a text, e.g. a paragraph or a heading.\n- Span: Piece of text with a set of marks, e.g. bold or italic.\n- Mark: A mark is a data structure that can be appliad to a span, e.g. a link or a comment.\n- Mark definition: A mark definition is a structure that describes a mark, a link or a comment.` - const target = `Portable Text is an agnostic abstraction of rich text that can be serialized into pretty much any markup language, be it HTML, Markdown, SSML, XML, etc. It is designed to be efficient for real-time collaborative interfaces, and makes it possible to annotate rich text with additional data structures recursively.\n\nPortable Text is built on the idea of rich text as an array of blocks, themselves arrays of child spans. Each block can have a style and a set of mark definitions, which describe data structures that can be applied on the children spans. Portable Text also allows for inserting arbitrary data objects in the array, only requiring _type-key. Portable Text also allows for custom content objects in the root array, enabling editing- and rendering environments to mix rich text with custom content types.\n\nPortable Text is a recursive composition of arrays and objects. In its simplest form it's an array of objects of a type with an array of children. Some definitions: \n- Block: A block is what's typically recognized as a section of a text, e.g. a paragraph or a heading.\n- Span: A span is a piece of text with a set of marks, e.g. bold or italic.\n- Mark: A mark is a data structure that can be applied to a span, e.g. a link or a comment.\n- Mark definition: A mark definition is a data structure that describes a mark, e.g. a link or a comment.` - const patch = getPteDmpPatch(stringifyPatches(makePatches(makeDiff(source, target)))) - const editor = getMockEditor({text: source}) - expect(diffMatchPatch(editor, patch)).toBe(true) - expect(editor.getText()).toBe(target) - }) - - test('should apply offset text differences correctly', () => { - const source = `This string has changes, but they occur somewhere near the end. That means we need to use an offset to get at the change, we cannot just rely on equality segaments in the generated diff.` - const target = `This string has changes, but they occur somewhere near the end. That means we need to use an offset to get at the change, we cannot just rely on equality segments in the generated diff.` - const patch = getPteDmpPatch(stringifyPatches(makePatches(makeDiff(source, target)))) - const editor = getMockEditor({text: source}) - expect(diffMatchPatch(editor, patch)).toBe(true) - expect(editor.getText()).toBe(target) - }) -}) - -function getPteDmpPatch( - value: string, - path: Path = [{_key: 'bA'}, 'children', {_key: 's1'}, 'text'], -): DiffMatchPatch { - return { - type: 'diffMatchPatch', - path, - origin: 'remote', - value, - } -} - -type MockEditorOptions = {children: PortableTextTextBlock[]} | {text: string} - -function getMockEditor(options: MockEditorOptions): Pick< - PortableTextSlateEditor, - 'children' | 'isTextBlock' | 'apply' | 'selection' | 'onChange' -> & { - getText: () => string -} { - let children: PortableTextBlock[] = 'children' in options ? options.children : [] - if (!('children' in options)) { - children = [ - { - _type: 'block', - _key: 'bA', - children: [{_type: 'span', _key: 's1', text: 'text' in options ? options.text : ''}], - markDefs: [], - }, - ] - } - - function getText(blockKey?: string) { - return children - .filter((child): child is PortableTextTextBlock => isPortableTextTextBlock(child)) - .filter((child) => (blockKey ? child._key === blockKey : true)) - .flatMap((block) => - block.children - .filter((span) => isPortableTextSpan(span)) - .map((span) => span.text) - .join(''), - ) - .join('\n\n') - } - - function isTextBlock(value: unknown): value is PortableTextTextBlock { - return isPortableTextTextBlock(value) - } - - function apply(operation: Operation): void { - if (operation.type !== 'insert_text' && operation.type !== 'remove_text') { - throw new Error(`Unexpected operation type ${operation.type}`) - } - - // Forcing for tests, theoretically can target non-PT blocks - const ptBlocks = children as PortableTextTextBlock[] - - const {type, path, offset, text} = operation - const [blockIndex, spanIndex] = path - const span = ptBlocks[blockIndex].children[spanIndex] - const current = span.text - - if (type === 'insert_text') { - const before = current.slice(0, offset) - const after = current.slice(offset) - span.text = `${before}${text}${after}` - } else if (type === 'remove_text') { - const before = current.slice(0, offset) - const after = current.slice(offset + text.length) - span.text = `${before}${after}` - } - } - - return { - selection: null, - getText, - children: children as Descendant[], - apply, - onChange: () => { - // NOOP - }, - isTextBlock, - } -} diff --git a/packages/@sanity/portable-text-editor/src/utils/__tests__/operationToPatches.test.ts b/packages/@sanity/portable-text-editor/src/utils/__tests__/operationToPatches.test.ts deleted file mode 100644 index 07914930391..00000000000 --- a/packages/@sanity/portable-text-editor/src/utils/__tests__/operationToPatches.test.ts +++ /dev/null @@ -1,421 +0,0 @@ -import {beforeEach, describe, expect, it} from '@jest/globals' -import {type PortableTextTextBlock} from '@sanity/types' -import {createEditor, type Descendant} from 'slate' - -import {PortableTextEditor, type PortableTextEditorProps} from '../..' -import {schemaType} from '../../editor/__tests__/PortableTextEditorTester' -import {defaultKeyGenerator} from '../../editor/hooks/usePortableTextEditorKeyGenerator' -import {withPlugins} from '../../editor/plugins' -import {getPortableTextMemberSchemaTypes} from '../getPortableTextMemberSchemaTypes' -import {createOperationToPatches} from '../operationToPatches' - -const portableTextFeatures = getPortableTextMemberSchemaTypes(schemaType) - -const operationToPatches = createOperationToPatches(portableTextFeatures) - -const {editor} = withPlugins(createEditor(), { - portableTextEditor: new PortableTextEditor({schemaType} as PortableTextEditorProps), - keyGenerator: defaultKeyGenerator, - readOnly: false, -}) - -const createDefaultValue = () => - [ - { - _type: 'myTestBlockType', - _key: '1f2e64b47787', - style: 'normal', - markDefs: [], - children: [ - {_type: 'span', _key: 'c130395c640c', text: '', marks: []}, - { - _key: '773866318fa8', - _type: 'someObject', - value: {title: 'The Object'}, - __inline: true, - children: [{_type: 'span', _key: 'bogus', text: '', marks: []}], - }, - {_type: 'span', _key: 'fd9b4a4e6c0b', text: '', marks: []}, - ], - }, - ] as Descendant[] - -describe('operationToPatches', () => { - beforeEach(() => { - editor.children = createDefaultValue() - editor.onChange() - }) - - it('translates void items correctly when splitting spans', () => { - expect( - operationToPatches.splitNodePatch( - editor, - { - type: 'split_node', - path: [0, 0], - position: 0, - properties: {_type: 'span', _key: 'c130395c640c', marks: []}, - }, - - createDefaultValue(), - ), - ).toMatchInlineSnapshot(` - Array [ - Object { - "items": Array [ - Object { - "_key": "773866318fa8", - "_type": "someObject", - "title": "The Object", - }, - ], - "path": Array [ - Object { - "_key": "1f2e64b47787", - }, - "children", - Object { - "_key": "c130395c640c", - }, - ], - "position": "after", - "type": "insert", - }, - Object { - "path": Array [ - Object { - "_key": "1f2e64b47787", - }, - "children", - Object { - "_key": "c130395c640c", - }, - "text", - ], - "type": "set", - "value": "", - }, - ] - `) - }) - - it('produce correct insert block patch', () => { - expect( - operationToPatches.insertNodePatch( - editor, - { - type: 'insert_node', - path: [0], - node: { - _type: 'someObject', - _key: 'c130395c640c', - value: {title: 'The Object'}, - __inline: false, - children: [{_key: '1', _type: 'span', text: '', marks: []}], - }, - }, - createDefaultValue(), - ), - ).toMatchInlineSnapshot(` - Array [ - Object { - "items": Array [ - Object { - "_key": "c130395c640c", - "_type": "someObject", - "title": "The Object", - }, - ], - "path": Array [ - Object { - "_key": "1f2e64b47787", - }, - ], - "position": "before", - "type": "insert", - }, - ] - `) - }) - - it('produce correct insert block patch with an empty editor', () => { - editor.children = [] - editor.onChange() - expect( - operationToPatches.insertNodePatch( - editor, - { - type: 'insert_node', - path: [0], - node: { - _type: 'someObject', - _key: 'c130395c640c', - value: {}, - __inline: false, - children: [{_key: '1', _type: 'span', text: '', marks: []}], - }, - }, - - [], - ), - ).toMatchInlineSnapshot(` - Array [ - Object { - "path": Array [], - "type": "setIfMissing", - "value": Array [], - }, - Object { - "items": Array [ - Object { - "_key": "c130395c640c", - "_type": "someObject", - }, - ], - "path": Array [ - 0, - ], - "position": "before", - "type": "insert", - }, - ] - `) - }) - - it('produce correct insert child patch', () => { - expect( - operationToPatches.insertNodePatch( - editor, - { - type: 'insert_node', - path: [0, 3], - node: { - _type: 'someObject', - _key: 'c130395c640c', - value: {title: 'The Object'}, - __inline: true, - children: [{_key: '1', _type: 'span', text: '', marks: []}], - }, - }, - - createDefaultValue(), - ), - ).toMatchInlineSnapshot(` - Array [ - Object { - "items": Array [ - Object { - "_key": "c130395c640c", - "_type": "someObject", - "title": "The Object", - }, - ], - "path": Array [ - Object { - "_key": "1f2e64b47787", - }, - "children", - Object { - "_key": "fd9b4a4e6c0b", - }, - ], - "position": "after", - "type": "insert", - }, - ] - `) - }) - - it('produce correct insert text patch', () => { - ;(editor.children[0] as PortableTextTextBlock).children[2].text = '1' - editor.onChange() - expect( - operationToPatches.insertTextPatch( - editor, - { - type: 'insert_text', - path: [0, 2], - text: '1', - offset: 0, - }, - - createDefaultValue(), - ), - ).toMatchInlineSnapshot(` - Array [ - Object { - "path": Array [ - Object { - "_key": "1f2e64b47787", - }, - "children", - Object { - "_key": "fd9b4a4e6c0b", - }, - "text", - ], - "type": "diffMatchPatch", - "value": "@@ -0,0 +1 @@ - +1 - ", - }, - ] - `) - }) - - it('produces correct remove text patch', () => { - const before = createDefaultValue() - ;(before[0] as PortableTextTextBlock).children[2].text = '1' - expect( - operationToPatches.removeTextPatch( - editor, - { - type: 'remove_text', - path: [0, 2], - text: '1', - offset: 1, - }, - - before, - ), - ).toMatchInlineSnapshot(` - Array [ - Object { - "path": Array [ - Object { - "_key": "1f2e64b47787", - }, - "children", - Object { - "_key": "fd9b4a4e6c0b", - }, - "text", - ], - "type": "diffMatchPatch", - "value": "@@ -1 +0,0 @@ - -1 - ", - }, - ] - `) - }) - - it('produces correct remove child patch', () => { - expect( - operationToPatches.removeNodePatch( - editor, - { - type: 'remove_node', - path: [0, 1], - node: { - _key: '773866318fa8', - _type: 'someObject', - value: {title: 'The object'}, - __inline: true, - children: [{_type: 'span', _key: 'bogus', text: '', marks: []}], - }, - }, - - createDefaultValue(), - ), - ).toMatchInlineSnapshot(` - Array [ - Object { - "path": Array [ - Object { - "_key": "1f2e64b47787", - }, - "children", - Object { - "_key": "773866318fa8", - }, - ], - "type": "unset", - }, - ] - `) - }) - - it('produce correct remove block patch', () => { - const val = createDefaultValue() - expect( - operationToPatches.removeNodePatch( - editor, - { - type: 'remove_node', - path: [0], - node: val[0], - }, - - val, - ), - ).toMatchInlineSnapshot(` - Array [ - Object { - "path": Array [ - Object { - "_key": "1f2e64b47787", - }, - ], - "type": "unset", - }, - ] - `) - }) - - it('produce correct merge node patch', () => { - const val = createDefaultValue() - ;(val[0] as PortableTextTextBlock).children.push({ - _type: 'span', - _key: 'r4wr323432', - text: '1234', - marks: [], - }) - const block = editor.children[0] as PortableTextTextBlock - block.children = block.children.splice(0, 3) - block.children[2].text = '1234' - editor.onChange() - expect( - operationToPatches.mergeNodePatch( - editor, - { - type: 'merge_node', - path: [0, 3], - position: 2, - properties: {text: '1234'}, - }, - - val, - ), - ).toMatchInlineSnapshot(` - Array [ - Object { - "path": Array [ - Object { - "_key": "1f2e64b47787", - }, - "children", - Object { - "_key": "fd9b4a4e6c0b", - }, - "text", - ], - "type": "set", - "value": "1234", - }, - Object { - "path": Array [ - Object { - "_key": "1f2e64b47787", - }, - "children", - Object { - "_key": "r4wr323432", - }, - ], - "type": "unset", - }, - ] - `) - }) -}) diff --git a/packages/@sanity/portable-text-editor/src/utils/__tests__/patchToOperations.test.ts b/packages/@sanity/portable-text-editor/src/utils/__tests__/patchToOperations.test.ts deleted file mode 100644 index a38b00cfa93..00000000000 --- a/packages/@sanity/portable-text-editor/src/utils/__tests__/patchToOperations.test.ts +++ /dev/null @@ -1,293 +0,0 @@ -import {beforeEach, describe, expect, it} from '@jest/globals' -import {noop} from 'lodash' -import {createEditor, type Descendant} from 'slate' - -import {keyGenerator, type Patch, PortableTextEditor} from '../..' -import {schemaType} from '../../editor/__tests__/PortableTextEditorTester' -import {withPlugins} from '../../editor/plugins' -import {createApplyPatch} from '../applyPatch' -import {getPortableTextMemberSchemaTypes} from '../getPortableTextMemberSchemaTypes' -import {VOID_CHILD_KEY} from '../values' - -const schemaTypes = getPortableTextMemberSchemaTypes(schemaType) - -const patchToOperations = createApplyPatch(schemaTypes) -const portableTextEditor = new PortableTextEditor({schemaType, onChange: noop}) - -const {editor} = withPlugins(createEditor(), { - portableTextEditor, - keyGenerator, - readOnly: false, -}) - -const createDefaultValue = (): Descendant[] => [ - { - _type: 'image', - _key: 'c01739b0d03b', - children: [ - { - _key: VOID_CHILD_KEY, - _type: 'span', - text: '', - marks: [], - }, - ], - __inline: false, - value: { - asset: { - _ref: 'image-f52f71bc1df46e080dabe43a8effe8ccfb5f21de-4032x3024-png', - _type: 'reference', - }, - }, - }, -] - -describe('operationToPatches', () => { - beforeEach(() => { - editor.onChange() - }) - - it('makes the correct operations for block objects', () => { - editor.children = createDefaultValue() - const patches = [ - {type: 'unset', path: [{_key: 'c01739b0d03b'}, 'hotspot'], origin: 'remote'}, - {type: 'unset', path: [{_key: 'c01739b0d03b'}, 'crop'], origin: 'remote'}, - { - type: 'set', - path: [{_key: 'c01739b0d03b'}, 'asset'], - value: { - _ref: 'image-b5681d9d0b2b6c922238e7c694500dd7c1349b19-256x256-jpg', - _type: 'reference', - }, - origin: 'remote', - }, - ] as Patch[] - patches.forEach((p) => { - patchToOperations(editor, p) - }) - expect(editor.children).toMatchInlineSnapshot(` - Array [ - Object { - "__inline": false, - "_key": "c01739b0d03b", - "_type": "image", - "children": Array [ - Object { - "_key": "${VOID_CHILD_KEY}", - "_type": "span", - "marks": Array [], - "text": "", - }, - ], - "value": Object { - "asset": Object { - "_ref": "image-f52f71bc1df46e080dabe43a8effe8ccfb5f21de-4032x3024-png", - "_type": "reference", - }, - }, - }, - ] - `) - }) - it('will not create operations for insertion inside object blocks', () => { - editor.children = [ - { - _type: 'someType', - _key: 'c01739b0d03b', - children: [ - { - _key: VOID_CHILD_KEY, - _type: 'span', - text: '', - marks: [], - }, - ], - __inline: false, - value: { - asset: { - _ref: 'image-f52f71bc1df46e080dabe43a8effe8ccfb5f21de-4032x3024-png', - _type: 'reference', - }, - nestedArray: [], - }, - }, - ] - const patches = [ - {type: 'insert', path: [{_key: 'c01739b0d03b'}, 'nestedArray'], origin: 'remote'}, - ] as Patch[] - patches.forEach((p) => { - patchToOperations(editor, p) - }) - expect(editor.children).toMatchInlineSnapshot(` - Array [ - Object { - "__inline": false, - "_key": "c01739b0d03b", - "_type": "someType", - "children": Array [ - Object { - "_key": "${VOID_CHILD_KEY}", - "_type": "span", - "marks": Array [], - "text": "", - }, - ], - "value": Object { - "asset": Object { - "_ref": "image-f52f71bc1df46e080dabe43a8effe8ccfb5f21de-4032x3024-png", - "_type": "reference", - }, - "nestedArray": Array [], - }, - }, - ] - `) - }) - it('will not create operations for removal inside object blocks', () => { - editor.children = [ - { - _type: 'someType', - _key: 'c01739b0d03b', - children: [ - { - _key: VOID_CHILD_KEY, - _type: 'span', - text: '', - marks: [], - }, - ], - __inline: false, - value: { - asset: { - _ref: 'image-f52f71bc1df46e080dabe43a8effe8ccfb5f21de-4032x3024-png', - _type: 'reference', - }, - nestedArray: [ - { - _key: 'foo', - _type: 'nestedValue', - }, - ], - }, - }, - ] - const patches = [ - {type: 'unset', path: [{_key: 'c01739b0d03b'}, 'nestedArray', 0], origin: 'remote'}, - ] as Patch[] - patches.forEach((p) => { - patchToOperations(editor, p) - }) - expect(editor.children).toMatchInlineSnapshot(` - Array [ - Object { - "__inline": false, - "_key": "c01739b0d03b", - "_type": "someType", - "children": Array [ - Object { - "_key": "${VOID_CHILD_KEY}", - "_type": "span", - "marks": Array [], - "text": "", - }, - ], - "value": Object { - "asset": Object { - "_ref": "image-f52f71bc1df46e080dabe43a8effe8ccfb5f21de-4032x3024-png", - "_type": "reference", - }, - "nestedArray": Array [ - Object { - "_key": "foo", - "_type": "nestedValue", - }, - ], - }, - }, - ] - `) - }) - it('will not create operations for setting data inside object blocks', () => { - editor.children = [ - { - _key: '1335959d4d03', - _type: 'block', - children: [ - { - _key: '9bd868adcd6b', - _type: 'span', - marks: [], - text: '1 ', - }, - { - _key: '6f75d593f3fc', - _type: 'span', - marks: ['11de7fcea659'], - text: '2', - }, - { - _key: '033618a7f081', - _type: 'span', - marks: [], - text: ' 3', - }, - ], - markDefs: [ - { - _key: '11de7fcea659', - _type: 'link', - }, - ], - style: 'normal', - }, - ] - const patches = [ - { - type: 'set', - path: [{_key: '1335959d4d03'}, 'markDefs', {_key: '11de7fcea659'}], - origin: 'remote', - value: {href: 'http://www.test.com'}, - }, - ] as Patch[] - patches.forEach((p) => { - patchToOperations(editor, p) - }) - expect(editor.children).toMatchInlineSnapshot(` - Array [ - Object { - "_key": "1335959d4d03", - "_type": "block", - "children": Array [ - Object { - "_key": "9bd868adcd6b", - "_type": "span", - "marks": Array [], - "text": "1 ", - }, - Object { - "_key": "6f75d593f3fc", - "_type": "span", - "marks": Array [ - "11de7fcea659", - ], - "text": "2", - }, - Object { - "_key": "033618a7f081", - "_type": "span", - "marks": Array [], - "text": " 3", - }, - ], - "markDefs": Array [ - Object { - "_key": "11de7fcea659", - "_type": "link", - }, - ], - "style": "normal", - }, - ] - `) - }) -}) diff --git a/packages/@sanity/portable-text-editor/src/utils/__tests__/ranges.test.ts b/packages/@sanity/portable-text-editor/src/utils/__tests__/ranges.test.ts deleted file mode 100644 index 486b1fa3d49..00000000000 --- a/packages/@sanity/portable-text-editor/src/utils/__tests__/ranges.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import {describe, expect, it} from '@jest/globals' -import {type InsertTextOperation, type Range} from 'slate' - -import {moveRangeByOperation} from '../ranges' - -describe('moveRangeByOperation', () => { - it('should move range when inserting text in front of it', () => { - const range: Range = {anchor: {path: [0, 0], offset: 1}, focus: {path: [0, 0], offset: 3}} - const operation: InsertTextOperation = { - type: 'insert_text', - path: [0, 0], - offset: 0, - text: 'foo', - } - const newRange = moveRangeByOperation(range, operation) - expect(newRange).toEqual({anchor: {path: [0, 0], offset: 4}, focus: {path: [0, 0], offset: 6}}) - }) -}) diff --git a/packages/@sanity/portable-text-editor/src/utils/__tests__/valueNormalization.test.tsx b/packages/@sanity/portable-text-editor/src/utils/__tests__/valueNormalization.test.tsx deleted file mode 100644 index eb5168c02be..00000000000 --- a/packages/@sanity/portable-text-editor/src/utils/__tests__/valueNormalization.test.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import {describe, expect, it, jest} from '@jest/globals' -import {render, waitFor} from '@testing-library/react' -import {createRef, type RefObject} from 'react' - -import {PortableTextEditorTester, schemaType} from '../../editor/__tests__/PortableTextEditorTester' -import {PortableTextEditor} from '../../editor/PortableTextEditor' - -describe('values: normalization', () => { - it("accepts incoming value with blocks without a style or markDefs prop, but doesn't leave them without them when editing them", async () => { - const editorRef: RefObject = createRef() - const initialValue = [ - { - _key: '5fc57af23597', - _type: 'myTestBlockType', - children: [ - { - _key: 'be1c67c6971a', - _type: 'span', - marks: [], - text: 'Hello', - }, - ], - markDefs: [], - }, - ] - const onChange = jest.fn() - render( - , - ) - await waitFor(() => { - if (editorRef.current) { - PortableTextEditor.focus(editorRef.current) - PortableTextEditor.select(editorRef.current, { - focus: {path: [{_key: '5fc57af23597'}, 'children', {_key: 'be1c67c6971a'}], offset: 0}, - anchor: {path: [{_key: '5fc57af23597'}, 'children', {_key: 'be1c67c6971a'}], offset: 5}, - }) - PortableTextEditor.toggleMark(editorRef.current, 'strong') - expect(PortableTextEditor.getValue(editorRef.current)).toEqual([ - { - _key: '5fc57af23597', - _type: 'myTestBlockType', - children: [ - { - _key: 'be1c67c6971a', - _type: 'span', - marks: ['strong'], - text: 'Hello', - }, - ], - markDefs: [], - style: 'normal', - }, - ]) - } - }) - }) -}) diff --git a/packages/@sanity/portable-text-editor/src/utils/__tests__/values.test.ts b/packages/@sanity/portable-text-editor/src/utils/__tests__/values.test.ts deleted file mode 100644 index c0dffb4f95e..00000000000 --- a/packages/@sanity/portable-text-editor/src/utils/__tests__/values.test.ts +++ /dev/null @@ -1,253 +0,0 @@ -import {describe, expect, it} from '@jest/globals' - -import {schemaType} from '../../editor/__tests__/PortableTextEditorTester' -import {getPortableTextMemberSchemaTypes} from '../getPortableTextMemberSchemaTypes' -import {fromSlateValue, toSlateValue} from '../values' - -const schemaTypes = getPortableTextMemberSchemaTypes(schemaType) - -describe('toSlateValue', () => { - it('checks undefined', () => { - const result = toSlateValue(undefined, {schemaTypes}) - expect(result).toHaveLength(0) - }) - - it('runs given empty array', () => { - const result = toSlateValue([], {schemaTypes}) - expect(result).toHaveLength(0) - }) - - it('given type is custom with no custom properties, should include an empty text property in children and an empty value', () => { - const result = toSlateValue( - [ - { - _type: 'image', - _key: '123', - }, - ], - {schemaTypes}, - ) - - expect(result).toMatchObject([ - { - _key: '123', - _type: 'image', - children: [ - { - text: '', - }, - ], - value: {}, - }, - ]) - }) - - it('given type is block', () => { - const result = toSlateValue( - [ - { - _type: schemaTypes.block.name, - _key: '123', - children: [ - { - _type: 'span', - _key: '1231', - text: '123', - }, - ], - }, - ], - {schemaTypes}, - ) - expect(result).toMatchInlineSnapshot(` -Array [ - Object { - "_key": "123", - "_type": "myTestBlockType", - "children": Array [ - Object { - "_key": "1231", - "_type": "span", - "text": "123", - }, - ], - "style": "normal", - }, -] -`) - }) - - it('given type is block and has custom object in children', () => { - const result = toSlateValue( - [ - { - _type: schemaTypes.block.name, - _key: '123', - children: [ - { - _type: 'span', - _key: '1231', - text: '123', - }, - { - _type: 'image', - _key: '1232', - asset: { - _ref: 'ref-123', - }, - }, - ], - }, - ], - {schemaTypes}, - ) - expect(result).toMatchInlineSnapshot(` -Array [ - Object { - "_key": "123", - "_type": "myTestBlockType", - "children": Array [ - Object { - "_key": "1231", - "_type": "span", - "text": "123", - }, - Object { - "__inline": true, - "_key": "1232", - "_type": "image", - "children": Array [ - Object { - "_key": "void-child", - "_type": "span", - "marks": Array [], - "text": "", - }, - ], - "value": Object { - "asset": Object { - "_ref": "ref-123", - }, - }, - }, - ], - "style": "normal", - }, -] -`) - }) -}) - -describe('fromSlateValue', () => { - it('runs given empty array', () => { - const result = fromSlateValue([], 'image') - expect(result).toHaveLength(0) - }) - - it('converts a slate value to portable text', () => { - const ptValue = fromSlateValue( - [ - { - _type: 'block', - _key: 'dr239u3', - children: [ - { - _type: 'span', - _key: '252f4swet', - marks: [], - text: 'Hey ', - }, - { - _type: 'image', - _key: 'e324t4s', - __inline: true, - children: [{_key: '1', _type: 'span', text: '', marks: []}], - value: { - _type: 'image', - _key: 'e324t4s', - asset: {_ref: '32423r32rewr3rwerwer'}, - }, - }, - ], - markDefs: [], - style: 'normal', - }, - { - _type: 'image', - _key: 'wer32434', - children: [{_key: '1', _type: 'span', text: '', marks: []}], - value: { - _type: 'image', - _key: 'wer32434', - asset: {_ref: 'werwer452423423'}, - }, - }, - ], - 'block', - ) - expect(ptValue).toEqual([ - { - _type: 'block', - _key: 'dr239u3', - children: [ - { - _type: 'span', - _key: '252f4swet', - marks: [], - text: 'Hey ', - }, - { - _type: 'image', - _key: 'e324t4s', - asset: {_ref: '32423r32rewr3rwerwer'}, - }, - ], - markDefs: [], - style: 'normal', - }, - { - _type: 'image', - _key: 'wer32434', - asset: {_ref: 'werwer452423423'}, - }, - ]) - }) - - it('has object equality', () => { - const keyMap = {} - const value = [ - { - _type: 'image', - _key: 'wer32434', - asset: {_ref: 'werwer452423423'}, - }, - { - _type: 'block', - _key: 'dr239u3', - children: [ - { - _type: 'span', - _key: '252f4swet', - marks: [], - text: 'Hey ', - }, - { - _type: 'image', - _key: 'e324t4s', - asset: {_ref: '32423r32rewr3rwerwer'}, - }, - ], - markDefs: [], - style: 'normal', - }, - ] - const toSlate1 = toSlateValue(value, {schemaTypes}, keyMap) - const toSlate2 = toSlateValue(value, {schemaTypes}, keyMap) - expect(toSlate1[0]).toBe(toSlate2[0]) - expect(toSlate1[1]).toBe(toSlate2[1]) - const fromSlate1 = fromSlateValue(toSlate1, 'block', keyMap) - const fromSlate2 = fromSlateValue(toSlate2, 'block', keyMap) - expect(fromSlate1[0]).toBe(fromSlate2[0]) - expect(fromSlate1[1]).toBe(fromSlate2[1]) - }) -}) diff --git a/packages/@sanity/portable-text-editor/src/utils/applyPatch.ts b/packages/@sanity/portable-text-editor/src/utils/applyPatch.ts deleted file mode 100644 index 3c4f2516281..00000000000 --- a/packages/@sanity/portable-text-editor/src/utils/applyPatch.ts +++ /dev/null @@ -1,407 +0,0 @@ -/* eslint-disable max-statements */ -import { - applyPatches as diffMatchPatchApplyPatches, - cleanupEfficiency, - DIFF_DELETE, - DIFF_EQUAL, - DIFF_INSERT, - makeDiff, - parsePatch, -} from '@sanity/diff-match-patch' -import { - type KeyedSegment, - type Path, - type PathSegment, - type PortableTextBlock, - type PortableTextChild, -} from '@sanity/types' -import {type Descendant, Element, type Node, type Path as SlatePath, Text, Transforms} from 'slate' - -import {applyAll} from '../patch/applyPatch' -import {type PortableTextMemberSchemaTypes, type PortableTextSlateEditor} from '../types/editor' -import { - type DiffMatchPatch, - type InsertPatch, - type Patch, - type SetPatch, - type UnsetPatch, -} from '../types/patch' -import {debugWithName} from './debug' -import {toSlateValue} from './values' -import {KEY_TO_SLATE_ELEMENT} from './weakMaps' - -const debug = debugWithName('applyPatches') -const debugVerbose = debug.enabled && true - -/** - * Creates a function that can apply a patch onto a PortableTextSlateEditor. - */ -export function createApplyPatch( - schemaTypes: PortableTextMemberSchemaTypes, -): (editor: PortableTextSlateEditor, patch: Patch) => boolean { - let previousPatch: Patch | undefined - - return function (editor: PortableTextSlateEditor, patch: Patch): boolean { - let changed = false - - // Save some CPU cycles by not stringifying unless enabled - if (debugVerbose) { - debug('\n\nNEW PATCH =============================================================') - debug(JSON.stringify(patch, null, 2)) - } - - try { - switch (patch.type) { - case 'insert': - changed = insertPatch(editor, patch, schemaTypes) - break - case 'unset': - changed = unsetPatch(editor, patch, previousPatch) - break - case 'set': - changed = setPatch(editor, patch) - break - case 'diffMatchPatch': - changed = diffMatchPatch(editor, patch) - break - default: - debug('Unhandled patch', patch.type) - } - } catch (err) { - console.error(err) - } - previousPatch = patch - return changed - } -} - -/** - * Apply a remote diff match patch to the current PTE instance. - * Note meant for external consumption, only exported for testing purposes. - * - * @param editor - Portable text slate editor instance - * @param patch - The PTE diff match patch operation to apply - * @returns true if the patch was applied, false otherwise - * @internal - */ -export function diffMatchPatch( - editor: Pick< - PortableTextSlateEditor, - 'children' | 'isTextBlock' | 'apply' | 'selection' | 'onChange' - >, - patch: DiffMatchPatch, -): boolean { - const {block, child, childPath} = findBlockAndChildFromPath(editor, patch.path) - if (!block) { - debug('Block not found') - return false - } - if (!child || !childPath) { - debug('Child not found') - return false - } - const isSpanTextDiffMatchPatch = - block && - editor.isTextBlock(block) && - patch.path.length === 4 && - patch.path[1] === 'children' && - patch.path[3] === 'text' - - if (!isSpanTextDiffMatchPatch || !Text.isText(child)) { - return false - } - - const patches = parsePatch(patch.value) - const [newValue] = diffMatchPatchApplyPatches(patches, child.text, {allowExceedingIndices: true}) - const diff = cleanupEfficiency(makeDiff(child.text, newValue), 5) - - debugState(editor, 'before') - let offset = 0 - for (const [op, text] of diff) { - if (op === DIFF_INSERT) { - editor.apply({type: 'insert_text', path: childPath, offset, text}) - offset += text.length - } else if (op === DIFF_DELETE) { - editor.apply({type: 'remove_text', path: childPath, offset: offset, text}) - } else if (op === DIFF_EQUAL) { - offset += text.length - } - } - debugState(editor, 'after') - - return true -} - -function insertPatch( - editor: PortableTextSlateEditor, - patch: InsertPatch, - schemaTypes: PortableTextMemberSchemaTypes, -) { - const { - block: targetBlock, - child: targetChild, - blockPath: targetBlockPath, - childPath: targetChildPath, - } = findBlockAndChildFromPath(editor, patch.path) - if (!targetBlock || !targetBlockPath) { - debug('Block not found') - return false - } - if (patch.path.length > 1 && patch.path[1] !== 'children') { - debug('Ignoring patch targeting void value') - return false - } - // Insert blocks - if (patch.path.length === 1) { - const {items, position} = patch - const blocksToInsert = toSlateValue( - items as PortableTextBlock[], - {schemaTypes}, - KEY_TO_SLATE_ELEMENT.get(editor), - ) as Descendant[] - const targetBlockIndex = targetBlockPath[0] - const normalizedIdx = position === 'after' ? targetBlockIndex + 1 : targetBlockIndex - debug(`Inserting blocks at path [${normalizedIdx}]`) - debugState(editor, 'before') - Transforms.insertNodes(editor, blocksToInsert, {at: [normalizedIdx]}) - debugState(editor, 'after') - return true - } - // Insert children - const {items, position} = patch - if (!targetChild || !targetChildPath) { - debug('Child not found') - return false - } - const childrenToInsert = - targetBlock && - toSlateValue( - [{...targetBlock, children: items as PortableTextChild[]}], - {schemaTypes}, - KEY_TO_SLATE_ELEMENT.get(editor), - ) - const targetChildIndex = targetChildPath[1] - const normalizedIdx = position === 'after' ? targetChildIndex + 1 : targetChildIndex - const childInsertPath = [targetChildPath[0], normalizedIdx] - debug(`Inserting children at path ${childInsertPath}`) - debugState(editor, 'before') - if (childrenToInsert && Element.isElement(childrenToInsert[0])) { - Transforms.insertNodes(editor, childrenToInsert[0].children, {at: childInsertPath}) - } - debugState(editor, 'after') - return true -} - -function setPatch(editor: PortableTextSlateEditor, patch: SetPatch) { - let value = patch.value - if (typeof patch.path[3] === 'string') { - value = {} - value[patch.path[3]] = patch.value - } - const {block, blockPath, child, childPath} = findBlockAndChildFromPath(editor, patch.path) - - if (!block) { - debug('Block not found') - return false - } - const isTextBlock = editor.isTextBlock(block) - - // Ignore patches targeting nested void data, like 'markDefs' - if (isTextBlock && patch.path.length > 1 && patch.path[1] !== 'children') { - debug('Ignoring setting void value') - return false - } - - debugState(editor, 'before') - - // If this is targeting a text block child - if (isTextBlock && child && childPath) { - if (Text.isText(value) && Text.isText(child)) { - const newText = child.text - const oldText = value.text - if (oldText !== newText) { - debug('Setting text property') - editor.apply({ - type: 'remove_text', - path: childPath, - offset: 0, - text: newText, - }) - editor.apply({ - type: 'insert_text', - path: childPath, - offset: 0, - text: value.text, - }) - // call OnChange here to emit the new selection - // the user's selection might be interfering with - editor.onChange() - } - } else { - debug('Setting non-text property') - editor.apply({ - type: 'set_node', - path: childPath, - properties: {}, - newProperties: value as Partial, - }) - } - return true - } else if (Element.isElement(block) && patch.path.length === 1 && blockPath) { - debug('Setting block property') - const {children, ...nextRest} = value as unknown as PortableTextBlock - // eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars - const {children: prevChildren, ...prevRest} = block || {children: undefined} - // Set any block properties - editor.apply({ - type: 'set_node', - path: blockPath, - properties: {...prevRest}, - newProperties: nextRest, - }) - // Replace the children in the block - // Note that children must be explicitly inserted, and can't be set with set_node - debug('Setting children') - block.children.forEach((c, cIndex) => { - editor.apply({ - type: 'remove_node', - path: blockPath.concat(block.children.length - 1 - cIndex), - node: c, - }) - }) - if (Array.isArray(children)) { - children.forEach((c, cIndex) => { - editor.apply({ - type: 'insert_node', - path: blockPath.concat(cIndex), - node: c, - }) - }) - } - } else if (block && 'value' in block) { - const newVal = applyAll([block.value], [patch])[0] - Transforms.setNodes(editor, {...block, value: newVal}, {at: blockPath}) - return true - } - debugState(editor, 'after') - return true -} - -function unsetPatch(editor: PortableTextSlateEditor, patch: UnsetPatch, previousPatch?: Patch) { - // Value - if (patch.path.length === 0) { - debug('Removing everything') - debugState(editor, 'before') - const previousSelection = editor.selection - Transforms.deselect(editor) - editor.children.forEach((c, i) => { - Transforms.removeNodes(editor, {at: [i]}) - }) - Transforms.insertNodes(editor, editor.pteCreateEmptyBlock()) - if (previousSelection) { - Transforms.select(editor, { - anchor: {path: [0, 0], offset: 0}, - focus: {path: [0, 0], offset: 0}, - }) - } - // call OnChange here to emit the new selection - editor.onChange() - debugState(editor, 'after') - return true - } - const {block, blockPath, child, childPath} = findBlockAndChildFromPath(editor, patch.path) - - // Single blocks - if (patch.path.length === 1) { - if (!block || !blockPath) { - debug('Block not found') - return false - } - const blockIndex = blockPath[0] - debug(`Removing block at path [${blockIndex}]`) - debugState(editor, 'before') - - Transforms.removeNodes(editor, {at: [blockIndex]}) - debugState(editor, 'after') - return true - } - - // Unset on text block children - if (editor.isTextBlock(block) && patch.path[1] === 'children' && patch.path.length === 3) { - if (!child || !childPath) { - debug('Child not found') - return false - } - debug(`Unsetting child at path ${JSON.stringify(childPath)}`) - debugState(editor, 'before') - if (debugVerbose) { - debug(`Removing child at path ${JSON.stringify(childPath)}`) - } - Transforms.removeNodes(editor, {at: childPath}) - debugState(editor, 'after') - return true - } - return false -} - -function isKeyedSegment(segment: PathSegment): segment is KeyedSegment { - return typeof segment === 'object' && '_key' in segment -} - -function debugState( - editor: Pick, - stateName: string, -) { - if (!debugVerbose) { - return - } - - debug(`Children ${stateName}:`, JSON.stringify(editor.children, null, 2)) - debug(`Selection ${stateName}: `, JSON.stringify(editor.selection, null, 2)) -} - -function findBlockFromPath( - editor: Pick< - PortableTextSlateEditor, - 'children' | 'isTextBlock' | 'apply' | 'selection' | 'onChange' - >, - path: Path, -): {block?: Descendant; path?: SlatePath} { - let blockIndex = -1 - const block = editor.children.find((node: Descendant, index: number) => { - const isMatch = isKeyedSegment(path[0]) ? node._key === path[0]._key : index === path[0] - if (isMatch) { - blockIndex = index - } - return isMatch - }) - if (!block) { - return {} - } - return {block, path: [blockIndex] as SlatePath} -} - -function findBlockAndChildFromPath( - editor: Pick< - PortableTextSlateEditor, - 'children' | 'isTextBlock' | 'apply' | 'selection' | 'onChange' - >, - path: Path, -): {child?: Descendant; childPath?: SlatePath; block?: Descendant; blockPath?: SlatePath} { - const {block, path: blockPath} = findBlockFromPath(editor, path) - if (!(Element.isElement(block) && path[1] === 'children')) { - return {block, blockPath, child: undefined, childPath: undefined} - } - let childIndex = -1 - const child = block.children.find((node, index: number) => { - const isMatch = isKeyedSegment(path[2]) ? node._key === path[2]._key : index === path[2] - if (isMatch) { - childIndex = index - } - return isMatch - }) - if (!child) { - return {block, blockPath, child: undefined, childPath: undefined} - } - return {block, child, blockPath, childPath: blockPath?.concat(childIndex) as SlatePath} -} diff --git a/packages/@sanity/portable-text-editor/src/utils/bufferUntil.ts b/packages/@sanity/portable-text-editor/src/utils/bufferUntil.ts deleted file mode 100644 index ae552eafc7e..00000000000 --- a/packages/@sanity/portable-text-editor/src/utils/bufferUntil.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {defer, EMPTY, type Observable, of, type OperatorFunction, switchMap, tap} from 'rxjs' - -export function bufferUntil( - emitWhen: (currentBuffer: T[]) => boolean, -): OperatorFunction { - return (source: Observable) => - defer(() => { - let buffer: T[] = [] // custom buffer - return source.pipe( - tap((v) => buffer.push(v)), // add values to buffer - switchMap(() => (emitWhen(buffer) ? of(buffer) : EMPTY)), // emit the buffer when the condition is met - tap(() => (buffer = [])), // clear the buffer - ) - }) -} diff --git a/packages/@sanity/portable-text-editor/src/utils/debug.ts b/packages/@sanity/portable-text-editor/src/utils/debug.ts deleted file mode 100644 index d4860fea6ca..00000000000 --- a/packages/@sanity/portable-text-editor/src/utils/debug.ts +++ /dev/null @@ -1,12 +0,0 @@ -import debug from 'debug' - -const rootName = 'sanity-pte:' - -export default debug(rootName) -export function debugWithName(name: string): debug.Debugger { - const namespace = `${rootName}${name}` - if (debug && debug.enabled(namespace)) { - return debug(namespace) - } - return debug(rootName) -} diff --git a/packages/@sanity/portable-text-editor/src/utils/getPortableTextMemberSchemaTypes.ts b/packages/@sanity/portable-text-editor/src/utils/getPortableTextMemberSchemaTypes.ts deleted file mode 100644 index d594d351270..00000000000 --- a/packages/@sanity/portable-text-editor/src/utils/getPortableTextMemberSchemaTypes.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { - type ArraySchemaType, - type BlockSchemaType, - type ObjectSchemaType, - type PortableTextBlock, - type SchemaType, - type SpanSchemaType, -} from '@sanity/types' - -import {type PortableTextMemberSchemaTypes} from '../types/editor' - -export function getPortableTextMemberSchemaTypes( - portableTextType: ArraySchemaType, -): PortableTextMemberSchemaTypes { - if (!portableTextType) { - throw new Error("Parameter 'portabletextType' missing (required)") - } - const blockType = portableTextType.of?.find(findBlockType) as BlockSchemaType | undefined - if (!blockType) { - throw new Error('Block type is not defined in this schema (required)') - } - const childrenField = blockType.fields?.find((field) => field.name === 'children') as - | {type: ArraySchemaType} - | undefined - if (!childrenField) { - throw new Error('Children field for block type found in schema (required)') - } - const ofType = childrenField.type.of - if (!ofType) { - throw new Error('Valid types for block children not found in schema (required)') - } - const spanType = ofType.find((memberType) => memberType.name === 'span') as - | ObjectSchemaType - | undefined - if (!spanType) { - throw new Error('Span type not found in schema (required)') - } - const inlineObjectTypes = (ofType.filter((memberType) => memberType.name !== 'span') || - []) as ObjectSchemaType[] - const blockObjectTypes = (portableTextType.of?.filter((field) => field.name !== blockType.name) || - []) as ObjectSchemaType[] - return { - styles: resolveEnabledStyles(blockType), - decorators: resolveEnabledDecorators(spanType), - lists: resolveEnabledListItems(blockType), - block: blockType, - span: spanType, - portableText: portableTextType, - inlineObjects: inlineObjectTypes, - blockObjects: blockObjectTypes, - annotations: (spanType as SpanSchemaType).annotations, - } -} - -function resolveEnabledStyles(blockType: ObjectSchemaType) { - const styleField = blockType.fields?.find((btField) => btField.name === 'style') - if (!styleField) { - throw new Error("A field with name 'style' is not defined in the block type (required).") - } - const textStyles = - styleField.type.options?.list && - styleField.type.options.list?.filter((style: {value: string}) => style.value) - if (!textStyles || textStyles.length === 0) { - throw new Error( - 'The style fields need at least one style ' + - "defined. I.e: {title: 'Normal', value: 'normal'}.", - ) - } - return textStyles -} - -function resolveEnabledDecorators(spanType: ObjectSchemaType) { - return (spanType as any).decorators -} - -function resolveEnabledListItems(blockType: ObjectSchemaType) { - const listField = blockType.fields?.find((btField) => btField.name === 'listItem') - if (!listField) { - throw new Error("A field with name 'listItem' is not defined in the block type (required).") - } - const listItems = - listField.type.options?.list && - listField.type.options.list.filter((list: {value: string}) => list.value) - if (!listItems) { - throw new Error('The list field need at least to be an empty array') - } - return listItems -} - -function findBlockType(type: SchemaType): BlockSchemaType | null { - if (type.type) { - return findBlockType(type.type) - } - - if (type.name === 'block') { - return type as BlockSchemaType - } - - return null -} diff --git a/packages/@sanity/portable-text-editor/src/utils/operationToPatches.ts b/packages/@sanity/portable-text-editor/src/utils/operationToPatches.ts deleted file mode 100644 index a38b61bf12c..00000000000 --- a/packages/@sanity/portable-text-editor/src/utils/operationToPatches.ts +++ /dev/null @@ -1,357 +0,0 @@ -import {type Path, type PortableTextSpan, type PortableTextTextBlock} from '@sanity/types' -import {get, isUndefined, omitBy} from 'lodash' -import { - type Descendant, - type InsertNodeOperation, - type InsertTextOperation, - type MergeNodeOperation, - type MoveNodeOperation, - type RemoveNodeOperation, - type RemoveTextOperation, - type SetNodeOperation, - type SplitNodeOperation, - Text, -} from 'slate' - -import {type PatchFunctions} from '../editor/plugins/createWithPatches' -import {diffMatchPatch, insert, set, setIfMissing, unset} from '../patch/PatchEvent' -import {type PortableTextMemberSchemaTypes, type PortableTextSlateEditor} from '../types/editor' -import {type InsertPosition, type Patch} from '../types/patch' -import {debugWithName} from './debug' -import {fromSlateValue} from './values' - -const debug = debugWithName('operationToPatches') -debug.enabled = false - -export function createOperationToPatches(types: PortableTextMemberSchemaTypes): PatchFunctions { - const textBlockName = types.block.name - function insertTextPatch( - editor: PortableTextSlateEditor, - operation: InsertTextOperation, - beforeValue: Descendant[], - ) { - if (debug.enabled) { - debug('Operation', JSON.stringify(operation, null, 2)) - } - const block = - editor.isTextBlock(editor.children[operation.path[0]]) && editor.children[operation.path[0]] - if (!block) { - throw new Error('Could not find block') - } - const textChild = - editor.isTextBlock(block) && - editor.isTextSpan(block.children[operation.path[1]]) && - (block.children[operation.path[1]] as PortableTextSpan) - if (!textChild) { - throw new Error('Could not find child') - } - const path: Path = [{_key: block._key}, 'children', {_key: textChild._key}, 'text'] - const prevBlock = beforeValue[operation.path[0]] - const prevChild = editor.isTextBlock(prevBlock) && prevBlock.children[operation.path[1]] - const prevText = editor.isTextSpan(prevChild) ? prevChild.text : '' - const patch = diffMatchPatch(prevText, textChild.text, path) - return patch.value.length ? [patch] : [] - } - - function removeTextPatch( - editor: PortableTextSlateEditor, - operation: RemoveTextOperation, - beforeValue: Descendant[], - ) { - const block = editor && editor.children[operation.path[0]] - if (!block) { - throw new Error('Could not find block') - } - const child = (editor.isTextBlock(block) && block.children[operation.path[1]]) || undefined - const textChild: PortableTextSpan | undefined = editor.isTextSpan(child) ? child : undefined - if (child && !textChild) { - throw new Error('Expected span') - } - if (!textChild) { - throw new Error('Could not find child') - } - const path: Path = [{_key: block._key}, 'children', {_key: textChild._key}, 'text'] - const beforeBlock = beforeValue[operation.path[0]] - const prevTextChild = editor.isTextBlock(beforeBlock) && beforeBlock.children[operation.path[1]] - const prevText = editor.isTextSpan(prevTextChild) && prevTextChild.text - const patch = diffMatchPatch(prevText || '', textChild.text, path) - return patch.value ? [patch] : [] - } - - function setNodePatch(editor: PortableTextSlateEditor, operation: SetNodeOperation) { - if (operation.path.length === 1) { - const block = editor.children[operation.path[0]] - if (typeof block._key !== 'string') { - throw new Error('Expected block to have a _key') - } - const setNode = omitBy( - {...editor.children[operation.path[0]], ...operation.newProperties}, - isUndefined, - ) as unknown as Descendant - return [set(fromSlateValue([setNode], textBlockName)[0], [{_key: block._key}])] - } else if (operation.path.length === 2) { - const block = editor.children[operation.path[0]] - if (editor.isTextBlock(block)) { - const child = block.children[operation.path[1]] - if (child) { - const blockKey = block._key - const childKey = child._key - const patches: Patch[] = [] - const keys = Object.keys(operation.newProperties) - keys.forEach((keyName) => { - // Special case for setting _key on a child. We have to target it by index and not the _key. - if (keys.length === 1 && keyName === '_key') { - const val = get(operation.newProperties, keyName) - patches.push( - set(val, [{_key: blockKey}, 'children', block.children.indexOf(child), keyName]), - ) - } else { - const val = get(operation.newProperties, keyName) - patches.push(set(val, [{_key: blockKey}, 'children', {_key: childKey}, keyName])) - } - }) - return patches - } - throw new Error('Could not find a valid child') - } - throw new Error('Could not find a valid block') - } else { - throw new Error(`Unexpected path encountered: ${JSON.stringify(operation.path)}`) - } - } - - function insertNodePatch( - editor: PortableTextSlateEditor, - operation: InsertNodeOperation, - beforeValue: Descendant[], - ): Patch[] { - const block = beforeValue[operation.path[0]] - const isTextBlock = editor.isTextBlock(block) - if (operation.path.length === 1) { - const position = operation.path[0] === 0 ? 'before' : 'after' - const beforeBlock = beforeValue[operation.path[0] - 1] - const targetKey = operation.path[0] === 0 ? block?._key : beforeBlock?._key - if (targetKey) { - return [ - insert([fromSlateValue([operation.node as Descendant], textBlockName)[0]], position, [ - {_key: targetKey}, - ]), - ] - } - return [ - setIfMissing(beforeValue, []), - insert([fromSlateValue([operation.node as Descendant], textBlockName)[0]], 'before', [ - operation.path[0], - ]), - ] - } else if (isTextBlock && operation.path.length === 2 && editor.children[operation.path[0]]) { - const position = - block.children.length === 0 || !block.children[operation.path[1] - 1] ? 'before' : 'after' - const node = {...operation.node} as Descendant - if (!node._type && Text.isText(node)) { - node._type = 'span' - node.marks = [] - } - const blk = fromSlateValue( - [ - { - _key: 'bogus', - _type: textBlockName, - children: [node], - }, - ], - textBlockName, - )[0] as PortableTextTextBlock - const child = blk.children[0] - return [ - insert([child], position, [ - {_key: block._key}, - 'children', - block.children.length <= 1 || !block.children[operation.path[1] - 1] - ? 0 - : {_key: block.children[operation.path[1] - 1]._key}, - ]), - ] - } - debug('Something was inserted into a void block. Not producing editor patches.') - return [] - } - - function splitNodePatch( - editor: PortableTextSlateEditor, - operation: SplitNodeOperation, - beforeValue: Descendant[], - ) { - const patches: Patch[] = [] - const splitBlock = editor.children[operation.path[0]] - if (!editor.isTextBlock(splitBlock)) { - throw new Error( - `Block with path ${JSON.stringify( - operation.path[0], - )} is not a text block and can't be split`, - ) - } - if (operation.path.length === 1) { - const oldBlock = beforeValue[operation.path[0]] - if (editor.isTextBlock(oldBlock)) { - const targetValue = fromSlateValue( - [editor.children[operation.path[0] + 1]], - textBlockName, - )[0] - if (targetValue) { - patches.push(insert([targetValue], 'after', [{_key: splitBlock._key}])) - const spansToUnset = oldBlock.children.slice(operation.position) - spansToUnset.forEach((span) => { - const path = [{_key: oldBlock._key}, 'children', {_key: span._key}] - patches.push(unset(path)) - }) - } - } - return patches - } - if (operation.path.length === 2) { - const splitSpan = splitBlock.children[operation.path[1]] - if (editor.isTextSpan(splitSpan)) { - const targetSpans = ( - fromSlateValue( - [ - { - ...splitBlock, - children: splitBlock.children.slice(operation.path[1] + 1, operation.path[1] + 2), - } as Descendant, - ], - textBlockName, - )[0] as PortableTextTextBlock - ).children - - patches.push( - insert(targetSpans, 'after', [ - {_key: splitBlock._key}, - 'children', - {_key: splitSpan._key}, - ]), - ) - patches.push( - set(splitSpan.text, [ - {_key: splitBlock._key}, - 'children', - {_key: splitSpan._key}, - 'text', - ]), - ) - } - return patches - } - return patches - } - - function removeNodePatch( - editor: PortableTextSlateEditor, - operation: RemoveNodeOperation, - beforeValue: Descendant[], - ) { - const block = beforeValue[operation.path[0]] - if (operation.path.length === 1) { - // Remove a single block - if (block && block._key) { - return [unset([{_key: block._key}])] - } - throw new Error('Block not found') - } else if (editor.isTextBlock(block) && operation.path.length === 2) { - const spanToRemove = - editor.isTextBlock(block) && block.children && block.children[operation.path[1]] - if (spanToRemove) { - return [unset([{_key: block._key}, 'children', {_key: spanToRemove._key}])] - } - debug('Span not found in editor trying to remove node') - return [] - } else { - debug('Not creating patch inside object block') - return [] - } - } - - function mergeNodePatch( - editor: PortableTextSlateEditor, - operation: MergeNodeOperation, - beforeValue: Descendant[], - ) { - const patches: Patch[] = [] - - const block = beforeValue[operation.path[0]] - const targetBlock = editor.children[operation.path[0]] - - if (operation.path.length === 1) { - if (block?._key) { - const newBlock = fromSlateValue([editor.children[operation.path[0] - 1]], textBlockName)[0] - patches.push(set(newBlock, [{_key: newBlock._key}])) - patches.push(unset([{_key: block._key}])) - } else { - throw new Error('Target key not found!') - } - } else if (operation.path.length === 2 && editor.isTextBlock(targetBlock)) { - const mergedSpan = - (editor.isTextBlock(block) && block.children[operation.path[1]]) || undefined - const targetSpan = targetBlock.children[operation.path[1] - 1] - if (editor.isTextSpan(targetSpan)) { - // Set the merged span with it's new value - patches.push( - set(targetSpan.text, [{_key: block._key}, 'children', {_key: targetSpan._key}, 'text']), - ) - if (mergedSpan) { - patches.push(unset([{_key: block._key}, 'children', {_key: mergedSpan._key}])) - } - } - } else { - debug("Void nodes can't be merged, not creating any patches") - } - return patches - } - - function moveNodePatch( - editor: PortableTextSlateEditor, - operation: MoveNodeOperation, - beforeValue: Descendant[], - ) { - const patches: Patch[] = [] - const block = beforeValue[operation.path[0]] - const targetBlock = beforeValue[operation.newPath[0]] - if (operation.path.length === 1) { - const position: InsertPosition = operation.path[0] > operation.newPath[0] ? 'before' : 'after' - patches.push(unset([{_key: block._key}])) - patches.push( - insert([fromSlateValue([block], textBlockName)[0]], position, [{_key: targetBlock._key}]), - ) - } else if ( - operation.path.length === 2 && - editor.isTextBlock(block) && - editor.isTextBlock(targetBlock) - ) { - const child = block.children[operation.path[1]] - const targetChild = targetBlock.children[operation.newPath[1]] - const position = operation.newPath[1] === targetBlock.children.length ? 'after' : 'before' - const childToInsert = (fromSlateValue([block], textBlockName)[0] as PortableTextTextBlock) - .children[operation.path[1]] - patches.push(unset([{_key: block._key}, 'children', {_key: child._key}])) - patches.push( - insert([childToInsert], position, [ - {_key: targetBlock._key}, - 'children', - {_key: targetChild._key}, - ]), - ) - } - return patches - } - - return { - insertNodePatch, - insertTextPatch, - mergeNodePatch, - moveNodePatch, - removeNodePatch, - removeTextPatch, - setNodePatch, - splitNodePatch, - } -} diff --git a/packages/@sanity/portable-text-editor/src/utils/patches.ts b/packages/@sanity/portable-text-editor/src/utils/patches.ts deleted file mode 100644 index 038af0b5b50..00000000000 --- a/packages/@sanity/portable-text-editor/src/utils/patches.ts +++ /dev/null @@ -1,36 +0,0 @@ -import {isEqual} from 'lodash' - -import {type Patch} from '../types/patch' - -/** - * Try to compact a set of patches - * - */ -export function compactPatches(patches: Patch[]) { - // If the last patch is unsetting everything, just do that - const lastPatch = patches.slice(-1)[0] - if (lastPatch && lastPatch.type === 'unset' && lastPatch.path.length === 0) { - return [lastPatch] - } - let finalPatches = patches - // Run through the patches and remove any redundant ones. - finalPatches = finalPatches.filter((patch, index) => { - if (!patch) { - return false - } - const nextPatch = finalPatches[index + 1] - if ( - nextPatch && - nextPatch.type === 'set' && - patch.type === 'set' && - isEqual(patch.path, nextPatch.path) - ) { - return false - } - return true - }) - if (finalPatches.length !== patches.length) { - return finalPatches - } - return patches -} diff --git a/packages/@sanity/portable-text-editor/src/utils/paths.ts b/packages/@sanity/portable-text-editor/src/utils/paths.ts deleted file mode 100644 index 9591076b1be..00000000000 --- a/packages/@sanity/portable-text-editor/src/utils/paths.ts +++ /dev/null @@ -1,60 +0,0 @@ -import {isKeySegment, type Path} from '@sanity/types' -import {isEqual} from 'lodash' -import {type Descendant, Editor, Element, type Path as SlatePath, type Point} from 'slate' - -import {type EditorSelectionPoint, type PortableTextMemberSchemaTypes} from '../types/editor' -import {type ObjectWithKeyAndType} from './ranges' - -export function createKeyedPath( - point: Point, - value: ObjectWithKeyAndType[] | undefined, - types: PortableTextMemberSchemaTypes, -): Path | null { - const blockPath = [point.path[0]] - if (!value) { - return null - } - const block = value[blockPath[0]] - if (!block) { - return null - } - const keyedBlockPath = [{_key: block._key}] - if (block._type !== types.block.name) { - return keyedBlockPath as Path - } - let keyedChildPath - const childPath = point.path.slice(0, 2) - const child = Array.isArray(block.children) && block.children[childPath[1]] - if (child) { - keyedChildPath = ['children', {_key: child._key}] - } - return (keyedChildPath ? [...keyedBlockPath, ...keyedChildPath] : keyedBlockPath) as Path -} - -export function createArrayedPath(point: EditorSelectionPoint, editor: Editor): SlatePath { - if (!editor) { - return [] - } - const [block, blockPath] = Array.from( - Editor.nodes(editor, { - at: [], - match: (n) => isKeySegment(point.path[0]) && (n as Descendant)._key === point.path[0]._key, - }), - )[0] || [undefined, undefined] - if (!block || !Element.isElement(block)) { - return [] - } - if (editor.isVoid(block)) { - return [blockPath[0], 0] - } - const childPath = [point.path[2]] - const childIndex = block.children.findIndex((child) => isEqual([{_key: child._key}], childPath)) - if (childIndex >= 0 && block.children[childIndex]) { - const child = block.children[childIndex] - if (Element.isElement(child) && editor.isVoid(child)) { - return blockPath.concat(childIndex).concat(0) - } - return blockPath.concat(childIndex) - } - return blockPath -} diff --git a/packages/@sanity/portable-text-editor/src/utils/ranges.ts b/packages/@sanity/portable-text-editor/src/utils/ranges.ts deleted file mode 100644 index d68b0508e38..00000000000 --- a/packages/@sanity/portable-text-editor/src/utils/ranges.ts +++ /dev/null @@ -1,77 +0,0 @@ -/* eslint-disable complexity */ -import {type BaseRange, type Editor, type Operation, Point, Range} from 'slate' - -import { - type EditorSelection, - type EditorSelectionPoint, - type PortableTextMemberSchemaTypes, -} from '../types/editor' -import {createArrayedPath, createKeyedPath} from './paths' - -export interface ObjectWithKeyAndType { - _key: string - _type: string - children?: ObjectWithKeyAndType[] -} - -export function toPortableTextRange( - value: ObjectWithKeyAndType[] | undefined, - range: BaseRange | Partial | null, - types: PortableTextMemberSchemaTypes, -): EditorSelection { - if (!range) { - return null - } - let anchor: EditorSelectionPoint | null = null - let focus: EditorSelectionPoint | null = null - const anchorPath = range.anchor && createKeyedPath(range.anchor, value, types) - if (anchorPath && range.anchor) { - anchor = { - path: anchorPath, - offset: range.anchor.offset, - } - } - const focusPath = range.focus && createKeyedPath(range.focus, value, types) - if (focusPath && range.focus) { - focus = { - path: focusPath, - offset: range.focus.offset, - } - } - const backward = Boolean(Range.isRange(range) ? Range.isBackward(range) : undefined) - return anchor && focus ? {anchor, focus, backward} : null -} - -export function toSlateRange(selection: EditorSelection, editor: Editor): Range | null { - if (!selection || !editor) { - return null - } - const anchor = { - path: createArrayedPath(selection.anchor, editor), - offset: selection.anchor.offset, - } - const focus = { - path: createArrayedPath(selection.focus, editor), - offset: selection.focus.offset, - } - if (focus.path.length === 0 || anchor.path.length === 0) { - return null - } - const range = anchor && focus ? {anchor, focus} : null - return range -} - -export function moveRangeByOperation(range: Range, operation: Operation): Range | null { - const anchor = Point.transform(range.anchor, operation) - const focus = Point.transform(range.focus, operation) - - if (anchor === null || focus === null) { - return null - } - - if (Point.equals(anchor, range.anchor) && Point.equals(focus, range.focus)) { - return range - } - - return {anchor, focus} -} diff --git a/packages/@sanity/portable-text-editor/src/utils/schema.ts b/packages/@sanity/portable-text-editor/src/utils/schema.ts deleted file mode 100644 index b52b887fe51..00000000000 --- a/packages/@sanity/portable-text-editor/src/utils/schema.ts +++ /dev/null @@ -1,8 +0,0 @@ -import {Schema} from '@sanity/schema' - -export function compileType(rawType: any) { - return Schema.compile({ - name: 'blockTypeSchema', - types: [rawType], - }).get(rawType.name) -} diff --git a/packages/@sanity/portable-text-editor/src/utils/selection.ts b/packages/@sanity/portable-text-editor/src/utils/selection.ts deleted file mode 100644 index b793aacf10d..00000000000 --- a/packages/@sanity/portable-text-editor/src/utils/selection.ts +++ /dev/null @@ -1,65 +0,0 @@ -import {type Path, type PortableTextBlock} from '@sanity/types' -import {isEqual} from 'lodash' - -import {type EditorSelection, type EditorSelectionPoint} from '../types/editor' - -export function normalizePoint( - point: EditorSelectionPoint, - value: PortableTextBlock[], -): EditorSelectionPoint | null { - if (!point || !value) { - return null - } - const newPath: Path = [] - let newOffset: number = point.offset || 0 - const blockKey = - typeof point.path[0] === 'object' && '_key' in point.path[0] && point.path[0]._key - const childKey = - typeof point.path[2] === 'object' && '_key' in point.path[2] && point.path[2]._key - const block: PortableTextBlock | undefined = value.find((blk) => blk._key === blockKey) - if (block) { - newPath.push({_key: block._key}) - } else { - return null - } - if (block && point.path[1] === 'children') { - if (!block.children || (Array.isArray(block.children) && block.children.length === 0)) { - return null - } - const child = - Array.isArray(block.children) && block.children.find((cld) => cld._key === childKey) - if (child) { - newPath.push('children') - newPath.push({_key: child._key}) - newOffset = - child.text && child.text.length >= point.offset - ? point.offset - : (child.text && child.text.length) || 0 - } else { - return null - } - } - return {path: newPath, offset: newOffset} -} - -export function normalizeSelection( - selection: EditorSelection, - value: PortableTextBlock[] | undefined, -): EditorSelection | null { - if (!selection || !value || value.length === 0) { - return null - } - let newAnchor: EditorSelectionPoint | null = null - let newFocus: EditorSelectionPoint | null = null - const {anchor, focus} = selection - if (anchor && value.find((blk) => isEqual({_key: blk._key}, anchor.path[0]))) { - newAnchor = normalizePoint(anchor, value) - } - if (focus && value.find((blk) => isEqual({_key: blk._key}, focus.path[0]))) { - newFocus = normalizePoint(focus, value) - } - if (newAnchor && newFocus) { - return {anchor: newAnchor, focus: newFocus, backward: selection.backward} - } - return null -} diff --git a/packages/@sanity/portable-text-editor/src/utils/ucs2Indices.ts b/packages/@sanity/portable-text-editor/src/utils/ucs2Indices.ts deleted file mode 100644 index 11ef7cbf277..00000000000 --- a/packages/@sanity/portable-text-editor/src/utils/ucs2Indices.ts +++ /dev/null @@ -1,67 +0,0 @@ -import {type Patch} from '@sanity/diff-match-patch' - -/** - * Takes a `patches` array as produced by diff-match-patch and adjusts the - * `start1` and `start2` properties so that they refer to UCS-2 index instead - * of a UTF-8 index. - * - * @param patches - The patches to adjust - * @param base - The base string to use for counting bytes - * @returns A new array of patches with adjusted indicies - * @beta - */ -export function adjustIndiciesToUcs2(patches: Patch[], base: string): Patch[] { - let byteOffset = 0 - let idx = 0 // index into the input. - - function advanceTo(target: number) { - for (; byteOffset < target; ) { - const codePoint = base.codePointAt(idx) - if (typeof codePoint === 'undefined') { - // Reached the end of the base string - the indicies won't be correct, - // but we also cannot advance any further to find a closer index. - return idx - } - - byteOffset += utf8len(codePoint) - - // This is encoded as a surrogate pair. - if (codePoint > 0xffff) { - idx += 2 - } else { - idx += 1 - } - } - - // Theoretically, we should have reached target - however, due to differences in - // `base` from the string that the patch was originally based upon, occurences - // _can_ happen where we go beyond the target due to surrogate pairs or similar. - // In the PTE, this is okayish - best effort matching is good enough. - return idx - } - - const adjusted: Patch[] = [] - for (const patch of patches) { - adjusted.push({ - diffs: patch.diffs.map((diff) => [...diff]), - start1: advanceTo(patch.start1), - start2: advanceTo(patch.start2), - utf8Start1: patch.utf8Start1, - utf8Start2: patch.utf8Start2, - length1: patch.length1, - length2: patch.length2, - utf8Length1: patch.utf8Length1, - utf8Length2: patch.utf8Length2, - }) - } - - return adjusted -} - -function utf8len(codePoint: number): 1 | 2 | 3 | 4 { - // See table at https://en.wikipedia.org/wiki/UTF-8 - if (codePoint <= 0x007f) return 1 - if (codePoint <= 0x07ff) return 2 - if (codePoint <= 0xffff) return 3 - return 4 -} diff --git a/packages/@sanity/portable-text-editor/src/utils/validateValue.ts b/packages/@sanity/portable-text-editor/src/utils/validateValue.ts deleted file mode 100644 index 4809b7717f1..00000000000 --- a/packages/@sanity/portable-text-editor/src/utils/validateValue.ts +++ /dev/null @@ -1,394 +0,0 @@ -import { - isPortableTextTextBlock, - type PortableTextBlock, - type PortableTextSpan, - type PortableTextTextBlock, -} from '@sanity/types' -import {flatten, isPlainObject, uniq} from 'lodash' - -import {insert, set, setIfMissing, unset} from '../patch/PatchEvent' -import {type InvalidValueResolution, type PortableTextMemberSchemaTypes} from '../types/editor' -import {EMPTY_MARKDEFS} from './values' - -export interface Validation { - valid: boolean - resolution: InvalidValueResolution | null - value: PortableTextBlock[] | undefined -} - -export function validateValue( - value: PortableTextBlock[] | undefined, - types: PortableTextMemberSchemaTypes, - keyGenerator: () => string, -): Validation { - let resolution: InvalidValueResolution | null = null - let valid = true - const validChildTypes = [types.span.name, ...types.inlineObjects.map((t) => t.name)] - const validBlockTypes = [types.block.name, ...types.blockObjects.map((t) => t.name)] - - // Undefined is allowed - if (value === undefined) { - return {valid: true, resolution: null, value} - } - // Only lengthy arrays are allowed in the editor. - if (!Array.isArray(value) || value.length === 0) { - return { - valid: false, - resolution: { - patches: [unset([])], - description: 'Editor value must be an array of Portable Text blocks, or undefined.', - action: 'Unset the value', - item: value, - - i18n: { - description: 'inputs.portable-text.invalid-value.not-an-array.description', - action: 'inputs.portable-text.invalid-value.not-an-array.action', - }, - }, - value, - } - } - if ( - value.some((blk: PortableTextBlock, index: number): boolean => { - // Is the block an object? - if (!isPlainObject(blk)) { - resolution = { - patches: [unset([index])], - description: `Block must be an object, got ${String(blk)}`, - action: `Unset invalid item`, - item: blk, - - i18n: { - description: 'inputs.portable-text.invalid-value.not-an-object.description', - action: 'inputs.portable-text.invalid-value.not-an-object.action', - values: {index}, - }, - } - return true - } - // Test that every block has a _key prop - if (!blk._key || typeof blk._key !== 'string') { - resolution = { - patches: [set({...blk, _key: keyGenerator()}, [index])], - description: `Block at index ${index} is missing required _key.`, - action: 'Set the block with a random _key value', - item: blk, - - i18n: { - description: 'inputs.portable-text.invalid-value.missing-key.description', - action: 'inputs.portable-text.invalid-value.missing-key.action', - values: {index}, - }, - } - return true - } - // Test that every block has valid _type - if (!blk._type || !validBlockTypes.includes(blk._type)) { - // Special case where block type is set to default 'block', but the block type is named something else according to the schema. - if (blk._type === 'block') { - const currentBlockTypeName = types.block.name - resolution = { - patches: [set({...blk, _type: currentBlockTypeName}, [{_key: blk._key}])], - description: `Block with _key '${blk._key}' has invalid type name '${blk._type}'. According to the schema, the block type name is '${currentBlockTypeName}'`, - action: `Use type '${currentBlockTypeName}'`, - item: blk, - - i18n: { - description: 'inputs.portable-text.invalid-value.incorrect-block-type.description', - action: 'inputs.portable-text.invalid-value.incorrect-block-type.action', - values: {key: blk._key, expectedTypeName: currentBlockTypeName}, - }, - } - return true - } - - // If the block has no `_type`, but aside from that is a valid Portable Text block - if (!blk._type && isPortableTextTextBlock({...blk, _type: types.block.name})) { - resolution = { - patches: [set({...blk, _type: types.block.name}, [{_key: blk._key}])], - description: `Block with _key '${blk._key}' is missing a type name. According to the schema, the block type name is '${types.block.name}'`, - action: `Use type '${types.block.name}'`, - item: blk, - - i18n: { - description: 'inputs.portable-text.invalid-value.missing-block-type.description', - action: 'inputs.portable-text.invalid-value.missing-block-type.action', - values: {key: blk._key, expectedTypeName: types.block.name}, - }, - } - return true - } - - if (!blk._type) { - resolution = { - patches: [unset([{_key: blk._key}])], - description: `Block with _key '${blk._key}' is missing an _type property`, - action: 'Remove the block', - item: blk, - - i18n: { - description: 'inputs.portable-text.invalid-value.missing-type.description', - action: 'inputs.portable-text.invalid-value.missing-type.action', - values: {key: blk._key}, - }, - } - return true - } - - resolution = { - patches: [unset([{_key: blk._key}])], - description: `Block with _key '${blk._key}' has invalid _type '${blk._type}'`, - action: 'Remove the block', - item: blk, - - i18n: { - description: 'inputs.portable-text.invalid-value.disallowed-type.description', - action: 'inputs.portable-text.invalid-value.disallowed-type.action', - values: {key: blk._key, typeName: blk._type}, - }, - } - return true - } - - // Test regular text blocks - if (blk._type === types.block.name) { - const textBlock = blk as PortableTextTextBlock - // Test that it has a valid children property (array) - if (textBlock.children && !Array.isArray(textBlock.children)) { - resolution = { - patches: [set({children: []}, [{_key: textBlock._key}])], - description: `Text block with _key '${textBlock._key}' has a invalid required property 'children'.`, - action: 'Reset the children property', - item: textBlock, - - i18n: { - description: - 'inputs.portable-text.invalid-value.missing-or-invalid-children.description', - action: 'inputs.portable-text.invalid-value.missing-or-invalid-children.action', - values: {key: textBlock._key}, - }, - } - return true - } - // Test that children is set and lengthy - if ( - textBlock.children === undefined || - (Array.isArray(textBlock.children) && textBlock.children.length === 0) - ) { - const newSpan = { - _type: types.span.name, - _key: keyGenerator(), - text: '', - marks: [], - } - resolution = { - autoResolve: true, - patches: [ - setIfMissing([], [{_key: blk._key}, 'children']), - insert([newSpan], 'after', [{_key: blk._key}, 'children', 0]), - ], - description: `Children for text block with _key '${blk._key}' is empty.`, - action: 'Insert an empty text', - item: blk, - - i18n: { - description: 'inputs.portable-text.invalid-value.empty-children.description', - action: 'inputs.portable-text.invalid-value.empty-children.action', - values: {key: blk._key}, - }, - } - return true - } - // Test that markDefs are valid if they exists - if (blk.markDefs && !Array.isArray(blk.markDefs)) { - resolution = { - patches: [set({...textBlock, markDefs: EMPTY_MARKDEFS}, [{_key: textBlock._key}])], - description: `Block has invalid required property 'markDefs'.`, - action: 'Add empty markDefs array', - item: textBlock, - - i18n: { - description: - 'inputs.portable-text.invalid-value.missing-or-invalid-markdefs.description', - action: 'inputs.portable-text.invalid-value.missing-or-invalid-markdefs.action', - values: {key: textBlock._key}, - }, - } - return true - } - const allUsedMarks = uniq( - flatten( - textBlock.children - .filter((cld) => cld._type === types.span.name) - .map((cld) => cld.marks || []), - ) as string[], - ) - - // Test that all markDefs are in use (remove orphaned markDefs) - if (Array.isArray(blk.markDefs) && blk.markDefs.length > 0) { - const unusedMarkDefs: string[] = uniq( - blk.markDefs.map((def) => def._key).filter((key) => !allUsedMarks.includes(key)), - ) - if (unusedMarkDefs.length > 0) { - resolution = { - autoResolve: true, - patches: unusedMarkDefs.map((markDefKey) => - unset([{_key: blk._key}, 'markDefs', {_key: markDefKey}]), - ), - description: `Block contains orphaned data (unused mark definitions): ${unusedMarkDefs.join( - ', ', - )}.`, - action: 'Remove unused mark definition item', - item: blk, - i18n: { - description: 'inputs.portable-text.invalid-value.orphaned-mark-defs.description', - action: 'inputs.portable-text.invalid-value.orphaned-mark-defs.action', - values: {key: blk._key, unusedMarkDefs: unusedMarkDefs.map((m) => m.toString())}, - }, - } - return true - } - } - - // Test that every annotation mark used has a definition - const annotationMarks = allUsedMarks.filter( - (mark) => !types.decorators.map((dec) => dec.value).includes(mark), - ) - const orphanedMarks = annotationMarks.filter( - (mark) => - textBlock.markDefs === undefined || - !textBlock.markDefs.find((def) => def._key === mark), - ) - if (orphanedMarks.length > 0) { - const spanChildren = textBlock.children.filter( - (cld) => - cld._type === types.span.name && - Array.isArray(cld.marks) && - cld.marks.some((mark) => orphanedMarks.includes(mark)), - ) as PortableTextSpan[] - if (spanChildren) { - const orphaned = orphanedMarks.join(', ') - resolution = { - autoResolve: true, - patches: spanChildren.map((child) => { - return set( - (child.marks || []).filter((cMrk) => !orphanedMarks.includes(cMrk)), - [{_key: blk._key}, 'children', {_key: child._key}, 'marks'], - ) - }), - description: `Block with _key '${blk._key}' contains marks (${orphaned}) not supported by the current content model.`, - action: 'Remove invalid marks', - item: blk, - - i18n: { - description: 'inputs.portable-text.invalid-value.orphaned-marks.description', - action: 'inputs.portable-text.invalid-value.orphaned-marks.action', - values: {key: blk._key, orphanedMarks: orphanedMarks.map((m) => m.toString())}, - }, - } - return true - } - } - - // Test every child - if ( - textBlock.children.some((child, cIndex: number) => { - if (!isPlainObject(child)) { - resolution = { - patches: [unset([{_key: blk._key}, 'children', cIndex])], - description: `Child at index '${cIndex}' in block with key '${blk._key}' is not an object.`, - action: 'Remove the item', - item: blk, - - i18n: { - description: 'inputs.portable-text.invalid-value.non-object-child.description', - action: 'inputs.portable-text.invalid-value.non-object-child.action', - values: {key: blk._key, index: cIndex}, - }, - } - return true - } - - if (!child._key || typeof child._key !== 'string') { - const newChild = {...child, _key: keyGenerator()} - resolution = { - autoResolve: true, - patches: [set(newChild, [{_key: blk._key}, 'children', cIndex])], - description: `Child at index ${cIndex} is missing required _key in block with _key ${blk._key}.`, - action: 'Set a new random _key on the object', - item: blk, - - i18n: { - description: 'inputs.portable-text.invalid-value.missing-child-key.description', - action: 'inputs.portable-text.invalid-value.missing-child-key.action', - values: {key: blk._key, index: cIndex}, - }, - } - return true - } - - // Verify that children have valid types - if (!child._type) { - resolution = { - patches: [unset([{_key: blk._key}, 'children', {_key: child._key}])], - description: `Child with _key '${child._key}' in block with key '${blk._key}' is missing '_type' property.`, - action: 'Remove the object', - item: blk, - - i18n: { - description: 'inputs.portable-text.invalid-value.missing-child-type.description', - action: 'inputs.portable-text.invalid-value.missing-child-type.action', - values: {key: blk._key, childKey: child._key}, - }, - } - return true - } - - if (!validChildTypes.includes(child._type)) { - resolution = { - patches: [unset([{_key: blk._key}, 'children', {_key: child._key}])], - description: `Child with _key '${child._key}' in block with key '${blk._key}' has invalid '_type' property (${child._type}).`, - action: 'Remove the object', - item: blk, - - i18n: { - description: - 'inputs.portable-text.invalid-value.disallowed-child-type.description', - action: 'inputs.portable-text.invalid-value.disallowed-child-type.action', - values: {key: blk._key, childKey: child._key, childType: child._type}, - }, - } - return true - } - - // Verify that spans have .text property that is a string - if (child._type === types.span.name && typeof child.text !== 'string') { - resolution = { - patches: [ - set({...child, text: ''}, [{_key: blk._key}, 'children', {_key: child._key}]), - ], - description: `Child with _key '${child._key}' in block with key '${blk._key}' has missing or invalid text property!`, - action: `Write an empty text property to the object`, - item: blk, - - i18n: { - description: 'inputs.portable-text.invalid-value.invalid-span-text.description', - action: 'inputs.portable-text.invalid-value.invalid-span-text.action', - values: {key: blk._key, childKey: child._key}, - }, - } - return true - } - return false - }) - ) { - valid = false - } - } - return false - }) - ) { - valid = false - } - return {valid, resolution, value} -} diff --git a/packages/@sanity/portable-text-editor/src/utils/values.ts b/packages/@sanity/portable-text-editor/src/utils/values.ts deleted file mode 100644 index 0c933e384b5..00000000000 --- a/packages/@sanity/portable-text-editor/src/utils/values.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { - type PathSegment, - type PortableTextBlock, - type PortableTextChild, - type PortableTextObject, - type PortableTextTextBlock, -} from '@sanity/types' -import {isEqual} from 'lodash' -import {type Descendant, Element, type Node, Text} from 'slate' - -import {type PortableTextMemberSchemaTypes} from '../types/editor' - -export const EMPTY_MARKDEFS: PortableTextObject[] = [] -export const EMPTY_MARKS: string[] = [] - -export const VOID_CHILD_KEY = 'void-child' - -type Partial = { - [P in keyof T]?: T[P] -} - -function keepObjectEquality( - object: PortableTextBlock | PortableTextChild, - keyMap: Record, -) { - const value = keyMap[object._key] - if (value && isEqual(object, value)) { - return value - } - keyMap[object._key] = object - return object -} - -export function toSlateValue( - value: PortableTextBlock[] | undefined, - {schemaTypes}: {schemaTypes: PortableTextMemberSchemaTypes}, - keyMap: Record = {}, -): Descendant[] { - if (value && Array.isArray(value)) { - return value.map((block) => { - const {_type, _key, ...rest} = block - const voidChildren = [{_key: VOID_CHILD_KEY, _type: 'span', text: '', marks: []}] - const isPortableText = block && block._type === schemaTypes.block.name - if (isPortableText) { - const textBlock = block as PortableTextTextBlock - let hasInlines = false - const hasMissingStyle = typeof textBlock.style === 'undefined' - const hasMissingMarkDefs = typeof textBlock.markDefs === 'undefined' - const hasMissingChildren = typeof textBlock.children === 'undefined' - - const children = (textBlock.children || []).map((child) => { - const {_type: cType, _key: cKey, ...cRest} = child - // Return 'slate' version of inline object where the actual - // value is stored in the `value` property. - // In slate, inline objects are represented as regular - // children with actual text node in order to be able to - // be selected the same way as the rest of the (text) content. - if (cType !== 'span') { - hasInlines = true - return keepObjectEquality( - { - _type: cType, - _key: cKey, - children: voidChildren, - value: cRest, - __inline: true, - }, - keyMap, - ) - } - // Original child object (span) - return child - }) - // Return original block - if ( - !hasMissingStyle && - !hasMissingMarkDefs && - !hasMissingChildren && - !hasInlines && - Element.isElement(block) - ) { - // Original object - return block - } - // TODO: remove this when we have a better way to handle missing style - if (hasMissingStyle) { - rest.style = schemaTypes.styles[0].value - } - return keepObjectEquality({_type, _key, ...rest, children}, keyMap) - } - return keepObjectEquality( - { - _type, - _key, - children: voidChildren, - value: rest, - }, - keyMap, - ) - }) as Descendant[] - } - return [] -} - -export function fromSlateValue( - value: Descendant[], - textBlockType: string, - keyMap: Record = {}, -): PortableTextBlock[] { - return value.map((block) => { - const {_key, _type} = block - if (!_key || !_type) { - throw new Error('Not a valid block') - } - if (_type === textBlockType && 'children' in block && Array.isArray(block.children) && _key) { - let hasInlines = false - const children = block.children.map((child) => { - const {_type: _cType} = child - if ('value' in child && _cType !== 'span') { - hasInlines = true - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const {value: v, _key: k, _type: t, __inline: _i, children: _c, ...rest} = child - return keepObjectEquality({...rest, ...v, _key: k as string, _type: t as string}, keyMap) - } - return child - }) - if (!hasInlines) { - return block as PortableTextBlock // Original object - } - return keepObjectEquality({...block, children, _key, _type}, keyMap) as PortableTextBlock - } - const blockValue = 'value' in block && block.value - return keepObjectEquality( - {_key, _type, ...(typeof blockValue === 'object' ? blockValue : {})}, - keyMap, - ) as PortableTextBlock - }) -} - -export function isEqualToEmptyEditor( - children: Descendant[] | PortableTextBlock[], - schemaTypes: PortableTextMemberSchemaTypes, -): boolean { - return ( - children === undefined || - (children && Array.isArray(children) && children.length === 0) || - (children && - Array.isArray(children) && - children.length === 1 && - Element.isElement(children[0]) && - children[0]._type === schemaTypes.block.name && - 'style' in children[0] && - children[0].style === schemaTypes.styles[0].value && - !('listItem' in children[0]) && - Array.isArray(children[0].children) && - children[0].children.length === 1 && - Text.isText(children[0].children[0]) && - children[0].children[0]._type === 'span' && - !children[0].children[0].marks?.join('') && - children[0].children[0].text === '') - ) -} - -export function findBlockAndIndexFromPath( - firstPathSegment: PathSegment, - children: (Node | Partial)[], -): [Element | undefined, number | undefined] { - let blockIndex = -1 - const isNumber = Number.isInteger(Number(firstPathSegment)) - if (isNumber) { - blockIndex = Number(firstPathSegment) - } else if (children) { - blockIndex = children.findIndex( - (blk) => Element.isElement(blk) && isEqual({_key: blk._key}, firstPathSegment), - ) - } - if (blockIndex > -1) { - return [children[blockIndex] as Element, blockIndex] - } - return [undefined, -1] -} - -export function findChildAndIndexFromPath( - secondPathSegment: PathSegment, - block: Element, -): [Element | Text | undefined, number] { - let childIndex = -1 - const isNumber = Number.isInteger(Number(secondPathSegment)) - if (isNumber) { - childIndex = Number(secondPathSegment) - } else { - childIndex = block.children.findIndex((child) => isEqual({_key: child._key}, secondPathSegment)) - } - if (childIndex > -1) { - return [block.children[childIndex] as Element | Text, childIndex] - } - return [undefined, -1] -} - -export function getValueOrInitialValue( - value: unknown, - initialValue: PortableTextBlock[], -): PortableTextBlock[] | undefined { - if (value && Array.isArray(value) && value.length > 0) { - return value - } - return initialValue -} diff --git a/packages/@sanity/portable-text-editor/src/utils/weakMaps.ts b/packages/@sanity/portable-text-editor/src/utils/weakMaps.ts deleted file mode 100644 index d1b6a6a4d2d..00000000000 --- a/packages/@sanity/portable-text-editor/src/utils/weakMaps.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {type Editor, type Element, type Range} from 'slate' - -import {type EditorSelection} from '..' - -// Is the editor currently receiving remote changes that are being applied to the content? -export const IS_PROCESSING_REMOTE_CHANGES: WeakMap = new WeakMap() -// Is the editor currently producing local changes that are not yet submitted? -export const IS_PROCESSING_LOCAL_CHANGES: WeakMap = new WeakMap() - -// Is the editor dragging something? -export const IS_DRAGGING: WeakMap = new WeakMap() -// Is the editor dragging a element? -export const IS_DRAGGING_BLOCK_ELEMENT: WeakMap = new WeakMap() - -// When dragging elements, this will be the target element -export const IS_DRAGGING_ELEMENT_TARGET: WeakMap = new WeakMap() -// Target position for dragging over a block -export const IS_DRAGGING_BLOCK_TARGET_POSITION: WeakMap = new WeakMap() - -export const KEY_TO_SLATE_ELEMENT: WeakMap = new WeakMap() -export const KEY_TO_VALUE_ELEMENT: WeakMap = new WeakMap() - -// Keep object relation to slate range in the portable-text-range -export const SLATE_TO_PORTABLE_TEXT_RANGE = new WeakMap() diff --git a/packages/@sanity/portable-text-editor/src/utils/withChanges.ts b/packages/@sanity/portable-text-editor/src/utils/withChanges.ts deleted file mode 100644 index b456f7d31fb..00000000000 --- a/packages/@sanity/portable-text-editor/src/utils/withChanges.ts +++ /dev/null @@ -1,25 +0,0 @@ -import {type Editor} from 'slate' - -import {IS_PROCESSING_LOCAL_CHANGES, IS_PROCESSING_REMOTE_CHANGES} from './weakMaps' - -export function withRemoteChanges(editor: Editor, fn: () => void): void { - const prev = isChangingRemotely(editor) || false - IS_PROCESSING_REMOTE_CHANGES.set(editor, true) - fn() - IS_PROCESSING_REMOTE_CHANGES.set(editor, prev) -} - -export function isChangingRemotely(editor: Editor): boolean | undefined { - return IS_PROCESSING_REMOTE_CHANGES.get(editor) -} - -export function withLocalChanges(editor: Editor, fn: () => void): void { - const prev = isChangingLocally(editor) || false - IS_PROCESSING_LOCAL_CHANGES.set(editor, true) - fn() - IS_PROCESSING_LOCAL_CHANGES.set(editor, prev) -} - -export function isChangingLocally(editor: Editor): boolean | undefined { - return IS_PROCESSING_LOCAL_CHANGES.get(editor) -} diff --git a/packages/@sanity/portable-text-editor/src/utils/withPreserveKeys.ts b/packages/@sanity/portable-text-editor/src/utils/withPreserveKeys.ts deleted file mode 100644 index f0328f5373f..00000000000 --- a/packages/@sanity/portable-text-editor/src/utils/withPreserveKeys.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {type Editor} from 'slate' - -export const PRESERVE_KEYS: WeakMap = new WeakMap() - -export function withPreserveKeys(editor: Editor, fn: () => void): void { - const prev = isPreservingKeys(editor) - PRESERVE_KEYS.set(editor, true) - fn() - PRESERVE_KEYS.set(editor, prev) -} - -export function isPreservingKeys(editor: Editor): boolean | undefined { - return PRESERVE_KEYS.get(editor) -} diff --git a/packages/@sanity/portable-text-editor/src/utils/withoutPatching.ts b/packages/@sanity/portable-text-editor/src/utils/withoutPatching.ts deleted file mode 100644 index 754a12f1eb4..00000000000 --- a/packages/@sanity/portable-text-editor/src/utils/withoutPatching.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {type Editor} from 'slate' - -export const PATCHING: WeakMap = new WeakMap() - -export function withoutPatching(editor: Editor, fn: () => void): void { - const prev = isPatching(editor) - PATCHING.set(editor, false) - fn() - PATCHING.set(editor, prev) -} - -export function isPatching(editor: Editor): boolean | undefined { - return PATCHING.get(editor) -} diff --git a/packages/@sanity/portable-text-editor/tsconfig.json b/packages/@sanity/portable-text-editor/tsconfig.json deleted file mode 100644 index 1a6d0a3f39e..00000000000 --- a/packages/@sanity/portable-text-editor/tsconfig.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "extends": "@repo/tsconfig/base.json", - "include": [ - "./src", - "./test", - "./node_modules/@sanity/types/src", - "./node_modules/@sanity/schema/src", - "./node_modules/@sanity/schema/typings", - "./node_modules/@sanity/util/src", - "./node_modules/@sanity/block-tools/src" - ], - "compilerOptions": { - "rootDir": ".", - "paths": { - "@sanity/block-tools": ["./node_modules/@sanity/block-tools/src/index.ts"], - "@sanity/schema/*": ["./node_modules/@sanity/schema/src/_exports/*"], - "@sanity/schema": ["./node_modules/@sanity/schema/src/_exports/index.ts"], - "@sanity/types": ["./node_modules/@sanity/types/src"], - "@sanity/util/*": ["./node_modules/@sanity/util/src/_exports/*"], - "@sanity/util": ["./node_modules/@sanity/util/src/_exports/index.ts"] - }, - - "isolatedModules": false - } -} diff --git a/packages/@sanity/portable-text-editor/tsconfig.lib.json b/packages/@sanity/portable-text-editor/tsconfig.lib.json deleted file mode 100644 index ff5b1fe6308..00000000000 --- a/packages/@sanity/portable-text-editor/tsconfig.lib.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "@repo/tsconfig/build.json", - "include": ["./src"], - "compilerOptions": { - "rootDir": ".", - "outDir": "./lib", - - "isolatedModules": false - } -} diff --git a/packages/@sanity/vision/.depcheckrc.json b/packages/@sanity/vision/.depcheckrc.json index bbb2f677b19..ba3e1cd01cc 100644 --- a/packages/@sanity/vision/.depcheckrc.json +++ b/packages/@sanity/vision/.depcheckrc.json @@ -9,7 +9,6 @@ "@sanity/codegen", "@sanity/mutator", "@sanity/diff", - "@sanity/portable-text-editor", "@sanity/codegen", "@sanity/schema", "@sanity/block-tools", diff --git a/packages/@sanity/vision/package.json b/packages/@sanity/vision/package.json index 0fda705d413..ee9e59c135c 100644 --- a/packages/@sanity/vision/package.json +++ b/packages/@sanity/vision/package.json @@ -80,7 +80,6 @@ "@sanity/diff": "workspace:*", "@sanity/migrate": "workspace:*", "@sanity/mutator": "workspace:*", - "@sanity/portable-text-editor": "workspace:*", "@sanity/schema": "workspace:*", "@sanity/types": "workspace:*", "@sanity/util": "workspace:*", diff --git a/packages/@sanity/vision/tsconfig.json b/packages/@sanity/vision/tsconfig.json index dd5e73642bf..4c96e3c3aed 100644 --- a/packages/@sanity/vision/tsconfig.json +++ b/packages/@sanity/vision/tsconfig.json @@ -8,7 +8,6 @@ "./node_modules/@sanity/cli/typings/*.d.ts", "./node_modules/@sanity/codegen/src", "./node_modules/@sanity/mutator/src", - "./node_modules/@sanity/portable-text-editor/src", "./node_modules/@sanity/schema/src", "./node_modules/@sanity/schema/typings", "./node_modules/@sanity/migrate/src", @@ -27,7 +26,6 @@ "@sanity/cli": ["./node_modules/@sanity/cli/src/index.ts"], "@sanity/codegen": ["./node_modules/@sanity/codegen/src/_exports/index.ts"], "@sanity/mutator": ["./node_modules/@sanity/mutator/src/index.ts"], - "@sanity/portable-text-editor": ["./node_modules/@sanity/portable-text-editor/src/index.ts"], "@sanity/schema/*": ["./node_modules/@sanity/schema/src/_exports/*"], "@sanity/schema": ["./node_modules/@sanity/schema/src/_exports/index.ts"], "@sanity/migrate": ["./node_modules/@sanity/migrate/src/_exports/index.ts"], diff --git a/packages/sanity/package.json b/packages/sanity/package.json index 68db2617e14..f647b3d443e 100644 --- a/packages/sanity/package.json +++ b/packages/sanity/package.json @@ -143,6 +143,7 @@ "@dnd-kit/sortable": "^7.0.1", "@dnd-kit/utilities": "^3.2.0", "@juggle/resize-observer": "^3.3.1", + "@portabletext/editor": "^1.0.7", "@portabletext/react": "^3.0.0", "@rexxars/react-json-inspector": "^8.0.1", "@sanity/asset-utils": "^1.2.5", @@ -162,7 +163,6 @@ "@sanity/logos": "^2.1.4", "@sanity/migrate": "3.48.1", "@sanity/mutator": "3.48.1", - "@sanity/portable-text-editor": "3.48.1", "@sanity/presentation": "1.16.0", "@sanity/schema": "3.48.1", "@sanity/telemetry": "^0.7.7", diff --git a/packages/sanity/playwright-ct/tests/formBuilder/inputs/PortableText/Input.spec.tsx b/packages/sanity/playwright-ct/tests/formBuilder/inputs/PortableText/Input.spec.tsx index 9a51b153403..9bc67464771 100644 --- a/packages/sanity/playwright-ct/tests/formBuilder/inputs/PortableText/Input.spec.tsx +++ b/packages/sanity/playwright-ct/tests/formBuilder/inputs/PortableText/Input.spec.tsx @@ -1,5 +1,5 @@ import {expect, test} from '@playwright/experimental-ct-react' -import {type EditorChange, type PortableTextEditor} from '@sanity/portable-text-editor' +import {type EditorChange, type PortableTextEditor} from '@portabletext/editor' import {type RefObject} from 'react' import {testHelpers} from '../../../utils/testHelpers' diff --git a/packages/sanity/playwright-ct/tests/formBuilder/inputs/PortableText/InputStory.tsx b/packages/sanity/playwright-ct/tests/formBuilder/inputs/PortableText/InputStory.tsx index 8261d1950fb..b9c3e28ff15 100644 --- a/packages/sanity/playwright-ct/tests/formBuilder/inputs/PortableText/InputStory.tsx +++ b/packages/sanity/playwright-ct/tests/formBuilder/inputs/PortableText/InputStory.tsx @@ -1,4 +1,4 @@ -import {type PortableTextEditor} from '@sanity/portable-text-editor' +import {type PortableTextEditor} from '@portabletext/editor' import {defineArrayMember, defineField, defineType} from '@sanity/types' import {createRef, type RefObject, useMemo, useState} from 'react' import {type InputProps, type PortableTextInputProps} from 'sanity' diff --git a/packages/sanity/playwright-ct/tests/formBuilder/inputs/PortableText/RangeDecorationStory.tsx b/packages/sanity/playwright-ct/tests/formBuilder/inputs/PortableText/RangeDecorationStory.tsx index f06f486692c..39e48166cdc 100644 --- a/packages/sanity/playwright-ct/tests/formBuilder/inputs/PortableText/RangeDecorationStory.tsx +++ b/packages/sanity/playwright-ct/tests/formBuilder/inputs/PortableText/RangeDecorationStory.tsx @@ -1,5 +1,5 @@ /* eslint-disable max-nested-callbacks */ -import {type EditorSelection, type RangeDecoration} from '@sanity/portable-text-editor' +import {type EditorSelection, type RangeDecoration} from '@portabletext/editor' import {defineArrayMember, defineField, defineType, type SanityDocument} from '@sanity/types' import {type PropsWithChildren, useEffect, useMemo, useState} from 'react' import {type InputProps, PortableTextInput, type PortableTextInputProps} from 'sanity' diff --git a/packages/sanity/src/core/comments/__workshop__/CommentInlineHighlightDebugStory.tsx b/packages/sanity/src/core/comments/__workshop__/CommentInlineHighlightDebugStory.tsx index 4dbd67ae20b..e673aac6fef 100644 --- a/packages/sanity/src/core/comments/__workshop__/CommentInlineHighlightDebugStory.tsx +++ b/packages/sanity/src/core/comments/__workshop__/CommentInlineHighlightDebugStory.tsx @@ -6,7 +6,7 @@ import { PortableTextEditable, PortableTextEditor, type RangeDecoration, -} from '@sanity/portable-text-editor' +} from '@portabletext/editor' import {Schema} from '@sanity/schema' import {defineArrayMember, defineField, isKeySegment, type PortableTextBlock} from '@sanity/types' import {Box, Button, Card, Code, Container, Flex, Label, Stack, Text} from '@sanity/ui' diff --git a/packages/sanity/src/core/comments/components/pte/comment-input/CommentInput.tsx b/packages/sanity/src/core/comments/components/pte/comment-input/CommentInput.tsx index d05e43d6ec3..4c3ba734bb8 100644 --- a/packages/sanity/src/core/comments/components/pte/comment-input/CommentInput.tsx +++ b/packages/sanity/src/core/comments/components/pte/comment-input/CommentInput.tsx @@ -3,7 +3,7 @@ import { keyGenerator, PortableTextEditor, type RenderBlockFunction, -} from '@sanity/portable-text-editor' +} from '@portabletext/editor' import {type CurrentUser, type PortableTextBlock} from '@sanity/types' import {type AvatarSize, focusFirstDescendant, focusLastDescendant, Stack} from '@sanity/ui' import { diff --git a/packages/sanity/src/core/comments/components/pte/comment-input/CommentInputInner.tsx b/packages/sanity/src/core/comments/components/pte/comment-input/CommentInputInner.tsx index 7adbaafd025..093297c7f54 100644 --- a/packages/sanity/src/core/comments/components/pte/comment-input/CommentInputInner.tsx +++ b/packages/sanity/src/core/comments/components/pte/comment-input/CommentInputInner.tsx @@ -1,4 +1,4 @@ -import {type RenderBlockFunction} from '@sanity/portable-text-editor' +import {type RenderBlockFunction} from '@portabletext/editor' import {type CurrentUser} from '@sanity/types' import {type AvatarSize, Box, Card, Flex, MenuDivider, Stack} from '@sanity/ui' // eslint-disable-next-line camelcase diff --git a/packages/sanity/src/core/comments/components/pte/comment-input/CommentInputProvider.tsx b/packages/sanity/src/core/comments/components/pte/comment-input/CommentInputProvider.tsx index 8a9cc1cd355..48c9568c601 100644 --- a/packages/sanity/src/core/comments/components/pte/comment-input/CommentInputProvider.tsx +++ b/packages/sanity/src/core/comments/components/pte/comment-input/CommentInputProvider.tsx @@ -1,8 +1,4 @@ -import { - type EditorSelection, - PortableTextEditor, - usePortableTextEditor, -} from '@sanity/portable-text-editor' +import {type EditorSelection, PortableTextEditor, usePortableTextEditor} from '@portabletext/editor' import {isPortableTextSpan, type Path} from '@sanity/types' import {type ReactNode, useCallback, useMemo, useState} from 'react' import {CommentInputContext} from 'sanity/_singletons' diff --git a/packages/sanity/src/core/comments/components/pte/comment-input/Editable.tsx b/packages/sanity/src/core/comments/components/pte/comment-input/Editable.tsx index 1b33b1bec6c..6bc1eb751b5 100644 --- a/packages/sanity/src/core/comments/components/pte/comment-input/Editable.tsx +++ b/packages/sanity/src/core/comments/components/pte/comment-input/Editable.tsx @@ -3,7 +3,7 @@ import { PortableTextEditable, type RenderBlockFunction, usePortableTextEditorSelection, -} from '@sanity/portable-text-editor' +} from '@portabletext/editor' import {isPortableTextSpan, isPortableTextTextBlock} from '@sanity/types' import {useClickOutside} from '@sanity/ui' // eslint-disable-next-line camelcase diff --git a/packages/sanity/src/core/comments/components/pte/render/renderBlock.tsx b/packages/sanity/src/core/comments/components/pte/render/renderBlock.tsx index 6c1ec6e83be..43f5fc56f42 100644 --- a/packages/sanity/src/core/comments/components/pte/render/renderBlock.tsx +++ b/packages/sanity/src/core/comments/components/pte/render/renderBlock.tsx @@ -1,4 +1,4 @@ -import {type RenderBlockFunction} from '@sanity/portable-text-editor' +import {type RenderBlockFunction} from '@portabletext/editor' import {NormalBlock} from '../blocks' diff --git a/packages/sanity/src/core/comments/components/pte/render/renderChild.tsx b/packages/sanity/src/core/comments/components/pte/render/renderChild.tsx index cb6d245687d..fd0d62d9222 100644 --- a/packages/sanity/src/core/comments/components/pte/render/renderChild.tsx +++ b/packages/sanity/src/core/comments/components/pte/render/renderChild.tsx @@ -1,4 +1,4 @@ -import {type BlockChildRenderProps, type RenderChildFunction} from '@sanity/portable-text-editor' +import {type BlockChildRenderProps, type RenderChildFunction} from '@portabletext/editor' import {MentionInlineBlock} from '../blocks' diff --git a/packages/sanity/src/core/comments/plugin/input/components/CommentsPortableTextInput.tsx b/packages/sanity/src/core/comments/plugin/input/components/CommentsPortableTextInput.tsx index 707c992be86..f9b57c276de 100644 --- a/packages/sanity/src/core/comments/plugin/input/components/CommentsPortableTextInput.tsx +++ b/packages/sanity/src/core/comments/plugin/input/components/CommentsPortableTextInput.tsx @@ -5,7 +5,7 @@ import { PortableTextEditor, type RangeDecoration, type RangeDecorationOnMovedDetails, -} from '@sanity/portable-text-editor' +} from '@portabletext/editor' import {isPortableTextTextBlock} from '@sanity/types' import {BoundaryElementProvider, Stack, usePortal} from '@sanity/ui' import * as PathUtils from '@sanity/util/paths' diff --git a/packages/sanity/src/core/comments/utils/inline-comments/buildRangeDecorationSelectionsFromComments.ts b/packages/sanity/src/core/comments/utils/inline-comments/buildRangeDecorationSelectionsFromComments.ts index 89661f89896..dd9799f8a40 100644 --- a/packages/sanity/src/core/comments/utils/inline-comments/buildRangeDecorationSelectionsFromComments.ts +++ b/packages/sanity/src/core/comments/utils/inline-comments/buildRangeDecorationSelectionsFromComments.ts @@ -1,3 +1,4 @@ +import {type RangeDecoration} from '@portabletext/editor' import { applyPatches, cleanupEfficiency, @@ -9,7 +10,6 @@ import { makePatches, type Patch, } from '@sanity/diff-match-patch' -import {type RangeDecoration} from '@sanity/portable-text-editor' import { isPortableTextSpan, isPortableTextTextBlock, diff --git a/packages/sanity/src/core/comments/utils/inline-comments/buildRangeDecorations.tsx b/packages/sanity/src/core/comments/utils/inline-comments/buildRangeDecorations.tsx index cd61478f95f..60ca294f3c9 100644 --- a/packages/sanity/src/core/comments/utils/inline-comments/buildRangeDecorations.tsx +++ b/packages/sanity/src/core/comments/utils/inline-comments/buildRangeDecorations.tsx @@ -1,7 +1,4 @@ -import { - type RangeDecoration, - type RangeDecorationOnMovedDetails, -} from '@sanity/portable-text-editor' +import {type RangeDecoration, type RangeDecorationOnMovedDetails} from '@portabletext/editor' import {type PortableTextBlock} from '@sanity/types' import {memo, useCallback, useEffect, useRef} from 'react' diff --git a/packages/sanity/src/core/comments/utils/inline-comments/buildTextSelectionFromFragment.ts b/packages/sanity/src/core/comments/utils/inline-comments/buildTextSelectionFromFragment.ts index c9ea4dc2bb0..26ada9d59bd 100644 --- a/packages/sanity/src/core/comments/utils/inline-comments/buildTextSelectionFromFragment.ts +++ b/packages/sanity/src/core/comments/utils/inline-comments/buildTextSelectionFromFragment.ts @@ -1,5 +1,5 @@ +import {type EditorSelection} from '@portabletext/editor' import {toPlainText} from '@portabletext/react' -import {type EditorSelection} from '@sanity/portable-text-editor' import { isKeySegment, isPortableTextSpan, diff --git a/packages/sanity/src/core/form/inputs/PortableText/BlockActions.tsx b/packages/sanity/src/core/form/inputs/PortableText/BlockActions.tsx index 80174f6d445..39bc5fa7fe8 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/BlockActions.tsx +++ b/packages/sanity/src/core/form/inputs/PortableText/BlockActions.tsx @@ -1,4 +1,4 @@ -import {PortableTextEditor, usePortableTextEditor} from '@sanity/portable-text-editor' +import {PortableTextEditor, usePortableTextEditor} from '@portabletext/editor' import {type PortableTextBlock} from '@sanity/types' import {useMemo} from 'react' import {styled} from 'styled-components' diff --git a/packages/sanity/src/core/form/inputs/PortableText/Compositor.tsx b/packages/sanity/src/core/form/inputs/PortableText/Compositor.tsx index 935d9736ae6..ad7d13e4192 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/Compositor.tsx +++ b/packages/sanity/src/core/form/inputs/PortableText/Compositor.tsx @@ -8,7 +8,7 @@ import { type OnPasteFn, type RangeDecoration, usePortableTextEditor, -} from '@sanity/portable-text-editor' +} from '@portabletext/editor' import {type Path, type PortableTextBlock, type PortableTextTextBlock} from '@sanity/types' import {Box, Portal, PortalProvider, useBoundaryElement, usePortal} from '@sanity/ui' import {type ReactNode, useCallback, useMemo, useState} from 'react' diff --git a/packages/sanity/src/core/form/inputs/PortableText/Editor.tsx b/packages/sanity/src/core/form/inputs/PortableText/Editor.tsx index 6800b628091..505b1cf9e68 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/Editor.tsx +++ b/packages/sanity/src/core/form/inputs/PortableText/Editor.tsx @@ -12,7 +12,7 @@ import { type RenderDecoratorFunction, type RenderListItemFunction, type RenderStyleFunction, -} from '@sanity/portable-text-editor' +} from '@portabletext/editor' import {type Path} from '@sanity/types' import {BoundaryElementProvider, useBoundaryElement, useGlobalKeyDown, useLayer} from '@sanity/ui' // eslint-disable-next-line camelcase diff --git a/packages/sanity/src/core/form/inputs/PortableText/InvalidValue.tsx b/packages/sanity/src/core/form/inputs/PortableText/InvalidValue.tsx index 8b3be1bcf26..5bc46fc9b3c 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/InvalidValue.tsx +++ b/packages/sanity/src/core/form/inputs/PortableText/InvalidValue.tsx @@ -1,4 +1,4 @@ -import {type InvalidValueResolution} from '@sanity/portable-text-editor' +import {type InvalidValueResolution} from '@portabletext/editor' import {useTelemetry} from '@sanity/telemetry/react' import { Box, diff --git a/packages/sanity/src/core/form/inputs/PortableText/PortableTextInput.tsx b/packages/sanity/src/core/form/inputs/PortableText/PortableTextInput.tsx index 132ca737fed..19486ac7d02 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/PortableTextInput.tsx +++ b/packages/sanity/src/core/form/inputs/PortableText/PortableTextInput.tsx @@ -9,7 +9,7 @@ import { PortableTextEditor, type RangeDecoration, type RenderEditableFunction, -} from '@sanity/portable-text-editor' +} from '@portabletext/editor' import {useTelemetry} from '@sanity/telemetry/react' import {isKeySegment, type PortableTextBlock} from '@sanity/types' import {Box, Flex, Text, useToast} from '@sanity/ui' diff --git a/packages/sanity/src/core/form/inputs/PortableText/__workshop__/PresenceInputStory.tsx b/packages/sanity/src/core/form/inputs/PortableText/__workshop__/PresenceInputStory.tsx index 17b761d32b8..d6c8296dc03 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/__workshop__/PresenceInputStory.tsx +++ b/packages/sanity/src/core/form/inputs/PortableText/__workshop__/PresenceInputStory.tsx @@ -4,7 +4,7 @@ import { PortableTextEditable, PortableTextEditor, type RenderBlockFunction, -} from '@sanity/portable-text-editor' +} from '@portabletext/editor' import {Schema} from '@sanity/schema' import {type PortableTextBlock} from '@sanity/types' import {Card, Container, Flex, Stack, Text} from '@sanity/ui' diff --git a/packages/sanity/src/core/form/inputs/PortableText/__workshop__/customSchema/blockActions.tsx b/packages/sanity/src/core/form/inputs/PortableText/__workshop__/customSchema/blockActions.tsx index 0da06d62062..d217ccb03c8 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/__workshop__/customSchema/blockActions.tsx +++ b/packages/sanity/src/core/form/inputs/PortableText/__workshop__/customSchema/blockActions.tsx @@ -1,5 +1,5 @@ +import {keyGenerator} from '@portabletext/editor' import {CopyIcon} from '@sanity/icons' -import {keyGenerator} from '@sanity/portable-text-editor' import {type PortableTextBlock, type PortableTextTextBlock} from '@sanity/types' import {memo, useCallback} from 'react' diff --git a/packages/sanity/src/core/form/inputs/PortableText/__workshop__/customSchema/values.ts b/packages/sanity/src/core/form/inputs/PortableText/__workshop__/customSchema/values.ts index 6b64832f87b..1d177dbae2f 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/__workshop__/customSchema/values.ts +++ b/packages/sanity/src/core/form/inputs/PortableText/__workshop__/customSchema/values.ts @@ -1,4 +1,4 @@ -import {keyGenerator as createKey} from '@sanity/portable-text-editor' +import {keyGenerator as createKey} from '@portabletext/editor' import {type PortableTextBlock} from '@sanity/types' import {words} from './words' diff --git a/packages/sanity/src/core/form/inputs/PortableText/hooks/useHotKeys.tsx b/packages/sanity/src/core/form/inputs/PortableText/hooks/useHotKeys.tsx index 9f734fdf3f1..3563f7582a6 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/hooks/useHotKeys.tsx +++ b/packages/sanity/src/core/form/inputs/PortableText/hooks/useHotKeys.tsx @@ -1,4 +1,4 @@ -import {type HotkeyOptions, usePortableTextEditor} from '@sanity/portable-text-editor' +import {type HotkeyOptions, usePortableTextEditor} from '@portabletext/editor' import {useMemo, useState} from 'react' // This hook will create final hotkeys for the editor from on those from props. diff --git a/packages/sanity/src/core/form/inputs/PortableText/hooks/useScrollSelectionIntoView.tsx b/packages/sanity/src/core/form/inputs/PortableText/hooks/useScrollSelectionIntoView.tsx index bbf797ada23..7d79d89a185 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/hooks/useScrollSelectionIntoView.tsx +++ b/packages/sanity/src/core/form/inputs/PortableText/hooks/useScrollSelectionIntoView.tsx @@ -1,4 +1,4 @@ -import {PortableTextEditor} from '@sanity/portable-text-editor' +import {PortableTextEditor} from '@portabletext/editor' import {useMemo} from 'react' import scrollIntoView from 'scroll-into-view-if-needed' diff --git a/packages/sanity/src/core/form/inputs/PortableText/hooks/useSpellCheck.tsx b/packages/sanity/src/core/form/inputs/PortableText/hooks/useSpellCheck.tsx index 886fb96d8ab..7717bfd2e8f 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/hooks/useSpellCheck.tsx +++ b/packages/sanity/src/core/form/inputs/PortableText/hooks/useSpellCheck.tsx @@ -1,4 +1,4 @@ -import {usePortableTextEditor} from '@sanity/portable-text-editor' +import {usePortableTextEditor} from '@portabletext/editor' import {useMemo} from 'react' export function useSpellCheck(): boolean { diff --git a/packages/sanity/src/core/form/inputs/PortableText/hooks/useTrackFocusPath.tsx b/packages/sanity/src/core/form/inputs/PortableText/hooks/useTrackFocusPath.tsx index 9bda42f6245..9ee5ae33a44 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/hooks/useTrackFocusPath.tsx +++ b/packages/sanity/src/core/form/inputs/PortableText/hooks/useTrackFocusPath.tsx @@ -2,7 +2,7 @@ import { PortableTextEditor, usePortableTextEditor, usePortableTextEditorSelection, -} from '@sanity/portable-text-editor' +} from '@portabletext/editor' import {isKeyedObject, type KeyedObject, type Path} from '@sanity/types' import {isEqual} from '@sanity/util/paths' import {useLayoutEffect} from 'react' diff --git a/packages/sanity/src/core/form/inputs/PortableText/object/Annotation.tsx b/packages/sanity/src/core/form/inputs/PortableText/object/Annotation.tsx index cf3aba4d53c..7d0bd20f525 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/object/Annotation.tsx +++ b/packages/sanity/src/core/form/inputs/PortableText/object/Annotation.tsx @@ -1,4 +1,4 @@ -import {PortableTextEditor, usePortableTextEditor} from '@sanity/portable-text-editor' +import {PortableTextEditor, usePortableTextEditor} from '@portabletext/editor' import {type ObjectSchemaType, type Path, type PortableTextObject} from '@sanity/types' import {isEqual} from '@sanity/util/paths' import { diff --git a/packages/sanity/src/core/form/inputs/PortableText/object/BlockObject.tsx b/packages/sanity/src/core/form/inputs/PortableText/object/BlockObject.tsx index 3f404a366a0..a0ec6ed88ef 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/object/BlockObject.tsx +++ b/packages/sanity/src/core/form/inputs/PortableText/object/BlockObject.tsx @@ -1,9 +1,5 @@ /* eslint-disable complexity */ -import { - type EditorSelection, - PortableTextEditor, - usePortableTextEditor, -} from '@sanity/portable-text-editor' +import {type EditorSelection, PortableTextEditor, usePortableTextEditor} from '@portabletext/editor' import {isImage, type ObjectSchemaType, type Path, type PortableTextBlock} from '@sanity/types' import {Box, Flex, type ResponsivePaddingProps} from '@sanity/ui' import {isEqual} from '@sanity/util/paths' diff --git a/packages/sanity/src/core/form/inputs/PortableText/object/InlineObject.tsx b/packages/sanity/src/core/form/inputs/PortableText/object/InlineObject.tsx index d1508a2eb99..4bea1a104c7 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/object/InlineObject.tsx +++ b/packages/sanity/src/core/form/inputs/PortableText/object/InlineObject.tsx @@ -1,8 +1,4 @@ -import { - type EditorSelection, - PortableTextEditor, - usePortableTextEditor, -} from '@sanity/portable-text-editor' +import {type EditorSelection, PortableTextEditor, usePortableTextEditor} from '@portabletext/editor' import { type ObjectSchemaType, type Path, diff --git a/packages/sanity/src/core/form/inputs/PortableText/presence-cursors/usePresenceCursorDecorations.tsx b/packages/sanity/src/core/form/inputs/PortableText/presence-cursors/usePresenceCursorDecorations.tsx index bfe5c85f2fe..5a990f5b835 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/presence-cursors/usePresenceCursorDecorations.tsx +++ b/packages/sanity/src/core/form/inputs/PortableText/presence-cursors/usePresenceCursorDecorations.tsx @@ -1,7 +1,4 @@ -import { - type RangeDecoration, - type RangeDecorationOnMovedDetails, -} from '@sanity/portable-text-editor' +import {type RangeDecoration, type RangeDecorationOnMovedDetails} from '@portabletext/editor' import {type Path} from '@sanity/types' import {startsWith} from '@sanity/util/paths' import {isEqual} from 'lodash' diff --git a/packages/sanity/src/core/form/inputs/PortableText/text/Decorator.tsx b/packages/sanity/src/core/form/inputs/PortableText/text/Decorator.tsx index c914e301858..04c8d6b0a7e 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/text/Decorator.tsx +++ b/packages/sanity/src/core/form/inputs/PortableText/text/Decorator.tsx @@ -1,4 +1,4 @@ -import {type BlockDecoratorRenderProps} from '@sanity/portable-text-editor' +import {type BlockDecoratorRenderProps} from '@portabletext/editor' import {type Theme} from '@sanity/ui' import {useCallback, useMemo} from 'react' import {css, styled} from 'styled-components' diff --git a/packages/sanity/src/core/form/inputs/PortableText/text/ListItem.tsx b/packages/sanity/src/core/form/inputs/PortableText/text/ListItem.tsx index 8d0b4a91f8c..2dc25131838 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/text/ListItem.tsx +++ b/packages/sanity/src/core/form/inputs/PortableText/text/ListItem.tsx @@ -1,4 +1,4 @@ -import {type BlockListItemRenderProps} from '@sanity/portable-text-editor' +import {type BlockListItemRenderProps} from '@portabletext/editor' import {useMemo} from 'react' import {type BlockListItemProps} from '../../../types' diff --git a/packages/sanity/src/core/form/inputs/PortableText/text/Style.tsx b/packages/sanity/src/core/form/inputs/PortableText/text/Style.tsx index 4482f301705..997fc30b8cb 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/text/Style.tsx +++ b/packages/sanity/src/core/form/inputs/PortableText/text/Style.tsx @@ -1,4 +1,4 @@ -import {type BlockStyleRenderProps} from '@sanity/portable-text-editor' +import {type BlockStyleRenderProps} from '@portabletext/editor' import {useCallback, useMemo} from 'react' import {type BlockStyleProps} from '../../../types' diff --git a/packages/sanity/src/core/form/inputs/PortableText/text/TextBlock.tsx b/packages/sanity/src/core/form/inputs/PortableText/text/TextBlock.tsx index 584419cda7e..cb16415a46d 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/text/TextBlock.tsx +++ b/packages/sanity/src/core/form/inputs/PortableText/text/TextBlock.tsx @@ -1,8 +1,4 @@ -import { - type EditorSelection, - PortableTextEditor, - usePortableTextEditor, -} from '@sanity/portable-text-editor' +import {type EditorSelection, PortableTextEditor, usePortableTextEditor} from '@portabletext/editor' import {type ObjectSchemaType, type Path, type PortableTextTextBlock} from '@sanity/types' import {Box, Flex, type ResponsivePaddingProps, Text} from '@sanity/ui' import {isEqual} from '@sanity/util/paths' diff --git a/packages/sanity/src/core/form/inputs/PortableText/toolbar/ActionMenu.tsx b/packages/sanity/src/core/form/inputs/PortableText/toolbar/ActionMenu.tsx index 603fefdbebd..8dd0551d57f 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/toolbar/ActionMenu.tsx +++ b/packages/sanity/src/core/form/inputs/PortableText/toolbar/ActionMenu.tsx @@ -2,7 +2,7 @@ import { PortableTextEditor, usePortableTextEditor, usePortableTextEditorSelection, -} from '@sanity/portable-text-editor' +} from '@portabletext/editor' import {isKeySegment} from '@sanity/types' import {memo, useCallback, useMemo} from 'react' diff --git a/packages/sanity/src/core/form/inputs/PortableText/toolbar/BlockStyleSelect.tsx b/packages/sanity/src/core/form/inputs/PortableText/toolbar/BlockStyleSelect.tsx index 051de925242..2c0a9a5bc8e 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/toolbar/BlockStyleSelect.tsx +++ b/packages/sanity/src/core/form/inputs/PortableText/toolbar/BlockStyleSelect.tsx @@ -1,5 +1,5 @@ +import {PortableTextEditor, usePortableTextEditor} from '@portabletext/editor' import {ChevronDownIcon} from '@sanity/icons' -import {PortableTextEditor, usePortableTextEditor} from '@sanity/portable-text-editor' import { Menu, // eslint-disable-next-line no-restricted-imports diff --git a/packages/sanity/src/core/form/inputs/PortableText/toolbar/InsertMenu.tsx b/packages/sanity/src/core/form/inputs/PortableText/toolbar/InsertMenu.tsx index ce329a84de9..c093f6d47aa 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/toolbar/InsertMenu.tsx +++ b/packages/sanity/src/core/form/inputs/PortableText/toolbar/InsertMenu.tsx @@ -1,4 +1,4 @@ -import {PortableTextEditor, usePortableTextEditor} from '@sanity/portable-text-editor' +import {PortableTextEditor, usePortableTextEditor} from '@portabletext/editor' import {upperFirst} from 'lodash' import {memo, useCallback, useMemo} from 'react' diff --git a/packages/sanity/src/core/form/inputs/PortableText/toolbar/Toolbar.tsx b/packages/sanity/src/core/form/inputs/PortableText/toolbar/Toolbar.tsx index 05fc209288e..bfa65e6ed20 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/toolbar/Toolbar.tsx +++ b/packages/sanity/src/core/form/inputs/PortableText/toolbar/Toolbar.tsx @@ -1,10 +1,10 @@ -import {CollapseIcon, ExpandIcon} from '@sanity/icons' import { type HotkeyOptions, PortableTextEditor, usePortableTextEditor, usePortableTextEditorSelection, -} from '@sanity/portable-text-editor' +} from '@portabletext/editor' +import {CollapseIcon, ExpandIcon} from '@sanity/icons' import {type ObjectSchemaType, type Path, type SchemaType} from '@sanity/types' import {Box, Flex, useElementRect, useToast} from '@sanity/ui' import {memo, type MouseEvent, useCallback, useMemo, useState} from 'react' diff --git a/packages/sanity/src/core/form/inputs/PortableText/toolbar/helpers.tsx b/packages/sanity/src/core/form/inputs/PortableText/toolbar/helpers.tsx index 7c126255192..58ae95fca7e 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/toolbar/helpers.tsx +++ b/packages/sanity/src/core/form/inputs/PortableText/toolbar/helpers.tsx @@ -1,3 +1,8 @@ +import { + type HotkeyOptions, + PortableTextEditor, + type PortableTextMemberSchemaTypes, +} from '@portabletext/editor' import { BlockElementIcon, BoldIcon, @@ -11,11 +16,6 @@ import { UnderlineIcon, UnknownIcon, } from '@sanity/icons' -import { - type HotkeyOptions, - PortableTextEditor, - type PortableTextMemberSchemaTypes, -} from '@sanity/portable-text-editor' import {type ObjectSchemaType} from '@sanity/types' import {capitalize, get} from 'lodash' import {type ComponentType} from 'react' diff --git a/packages/sanity/src/core/form/inputs/PortableText/toolbar/hooks.ts b/packages/sanity/src/core/form/inputs/PortableText/toolbar/hooks.ts index 8fd694aec05..049f5bc7830 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/toolbar/hooks.ts +++ b/packages/sanity/src/core/form/inputs/PortableText/toolbar/hooks.ts @@ -3,7 +3,7 @@ import { PortableTextEditor, usePortableTextEditor, usePortableTextEditorSelection, -} from '@sanity/portable-text-editor' +} from '@portabletext/editor' import { type ObjectSchemaType, type Path, diff --git a/packages/sanity/src/core/form/inputs/PortableText/toolbar/types.ts b/packages/sanity/src/core/form/inputs/PortableText/toolbar/types.ts index 4588ac76c84..5aff458bbe8 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/toolbar/types.ts +++ b/packages/sanity/src/core/form/inputs/PortableText/toolbar/types.ts @@ -1,4 +1,4 @@ -// import {Type} from '@sanity/portable-text-editor' +// import {Type} from '@portabletext/editor' import {type ObjectSchemaType} from '@sanity/types' import {type ComponentType, type ElementType, type ReactNode} from 'react' diff --git a/packages/sanity/src/core/form/types/inputProps.ts b/packages/sanity/src/core/form/types/inputProps.ts index 2c2b5e37a5b..49dbf3eb8ca 100644 --- a/packages/sanity/src/core/form/types/inputProps.ts +++ b/packages/sanity/src/core/form/types/inputProps.ts @@ -6,7 +6,7 @@ import { type OnPasteFn, type PortableTextEditor, type RangeDecoration, -} from '@sanity/portable-text-editor' +} from '@portabletext/editor' import { type ArraySchemaType, type BooleanSchemaType, diff --git a/packages/sanity/src/core/presence/types.ts b/packages/sanity/src/core/presence/types.ts index e4f81f0c7d4..2a661a29a77 100644 --- a/packages/sanity/src/core/presence/types.ts +++ b/packages/sanity/src/core/presence/types.ts @@ -1,4 +1,4 @@ -import {type EditorSelection} from '@sanity/portable-text-editor' +import {type EditorSelection} from '@portabletext/editor' import {type Path, type User} from '@sanity/types' import {type Session, type Status} from '../store/_legacy' diff --git a/packages/sanity/src/core/store/_legacy/presence/types.ts b/packages/sanity/src/core/store/_legacy/presence/types.ts index 6d926bc0506..d9a06e09b91 100644 --- a/packages/sanity/src/core/store/_legacy/presence/types.ts +++ b/packages/sanity/src/core/store/_legacy/presence/types.ts @@ -1,4 +1,4 @@ -import {type EditorSelection} from '@sanity/portable-text-editor' +import {type EditorSelection} from '@portabletext/editor' import {type Path, type User} from '@sanity/types' /** @internal */ diff --git a/packages/sanity/src/core/tasks/components/form/fields/descriptionInput/render/renderBlock.tsx b/packages/sanity/src/core/tasks/components/form/fields/descriptionInput/render/renderBlock.tsx index ba42af78491..39610764c0d 100644 --- a/packages/sanity/src/core/tasks/components/form/fields/descriptionInput/render/renderBlock.tsx +++ b/packages/sanity/src/core/tasks/components/form/fields/descriptionInput/render/renderBlock.tsx @@ -1,4 +1,4 @@ -import {type RenderBlockFunction} from '@sanity/portable-text-editor' +import {type RenderBlockFunction} from '@portabletext/editor' import {DescriptionInputBlock} from '../blocks' diff --git a/packages/sanity/tsconfig.json b/packages/sanity/tsconfig.json index ffddd6e4518..cee71e261a8 100644 --- a/packages/sanity/tsconfig.json +++ b/packages/sanity/tsconfig.json @@ -13,7 +13,6 @@ "./node_modules/@sanity/cli/typings/deepSortObject.d.ts", "./node_modules/@sanity/codegen/src", "./node_modules/@sanity/mutator/src", - "./node_modules/@sanity/portable-text-editor/src", "./node_modules/@sanity/schema/src", "./node_modules/@sanity/schema/typings", "./node_modules/@sanity/migrate/src", @@ -29,7 +28,6 @@ "@sanity/cli": ["./node_modules/@sanity/cli/src/index.ts"], "@sanity/codegen": ["./node_modules/@sanity/codegen/src/_exports/index.ts"], "@sanity/mutator": ["./node_modules/@sanity/mutator/src/index.ts"], - "@sanity/portable-text-editor": ["./node_modules/@sanity/portable-text-editor/src/index.ts"], "@sanity/schema/*": ["./node_modules/@sanity/schema/src/_exports/*"], "@sanity/schema": ["./node_modules/@sanity/schema/src/_exports/index.ts"], "@sanity/migrate": ["./node_modules/@sanity/migrate/src/_exports/index.ts"], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be77d5a7e42..1ff5baad9de 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,7 +48,7 @@ importers: version: link:packages/@repo/tsconfig '@sanity/client': specifier: ^6.20.0 - version: 6.20.0 + version: 6.20.0(debug@4.3.5) '@sanity/eslint-config-i18n': specifier: 1.0.0 version: 1.0.0(eslint@8.57.0)(typescript@5.4.5) @@ -57,7 +57,7 @@ importers: version: 4.0.0(eslint@8.57.0)(typescript@5.4.5) '@sanity/pkg-utils': specifier: 6.9.3 - version: 6.9.3(@types/node@18.19.31)(typescript@5.4.5) + version: 6.9.3(@types/node@18.19.31)(debug@4.3.5)(typescript@5.4.5) '@sanity/prettier-config': specifier: ^1.0.2 version: 1.0.2(prettier@3.3.2) @@ -66,7 +66,7 @@ importers: version: 0.0.1-alpha.1 '@sanity/tsdoc': specifier: 1.0.72 - version: 1.0.72(@types/node@18.19.31)(react-dom@18.3.1)(react-is@18.3.1)(react@18.3.1)(sanity@packages+sanity)(styled-components@6.1.11) + version: 1.0.72(@types/node@18.19.31)(debug@4.3.5)(react-dom@18.3.1)(react-is@18.3.1)(react@18.3.1)(sanity@packages+sanity)(styled-components@6.1.11) '@sanity/uuid': specifier: ^3.0.2 version: 3.0.2 @@ -129,7 +129,7 @@ importers: version: 7.1.2(@typescript-eslint/eslint-plugin@7.11.0)(@typescript-eslint/parser@7.11.0)(eslint-plugin-import@2.29.1)(eslint-plugin-react-hooks@4.6.2)(eslint-plugin-react@7.34.2)(eslint@8.57.0) eslint-config-turbo: specifier: ^2.0.4 - version: 2.0.4(eslint@8.57.0) + version: 2.0.6(eslint@8.57.0) eslint-import-resolver-typescript: specifier: ^3.6.1 version: 3.6.1(@typescript-eslint/parser@7.11.0)(eslint-plugin-import@2.29.1)(eslint@8.57.0) @@ -219,7 +219,7 @@ importers: version: 7.6.2 turbo: specifier: ^2.0.4 - version: 2.0.4 + version: 2.0.6 typescript: specifier: 5.4.5 version: 5.4.5 @@ -427,6 +427,9 @@ importers: dev/test-studio: dependencies: + '@portabletext/editor': + specifier: ^1.0.7 + version: 1.0.7(@sanity/block-tools@packages+@sanity+block-tools)(@sanity/schema@packages+@sanity+schema)(@sanity/types@packages+@sanity+types)(@sanity/util@packages+@sanity+util)(react-dom@18.3.1)(react@18.3.1)(rxjs@7.8.1)(styled-components@6.1.11) '@portabletext/react': specifier: ^3.0.0 version: 3.1.0(react@18.3.1) @@ -447,7 +450,7 @@ importers: version: link:../../packages/@sanity/block-tools '@sanity/client': specifier: ^6.20.0 - version: 6.20.0 + version: 6.20.0(debug@4.3.5) '@sanity/color': specifier: ^3.0.0 version: 3.0.6 @@ -481,9 +484,6 @@ importers: '@sanity/migrate': specifier: workspace:* version: link:../../packages/@sanity/migrate - '@sanity/portable-text-editor': - specifier: workspace:* - version: link:../../packages/@sanity/portable-text-editor '@sanity/preview-url-secret': specifier: ^1.6.1 version: 1.6.17(@sanity/client@6.20.0) @@ -492,7 +492,7 @@ importers: version: 1.10.3(@sanity/client@6.20.0)(react@18.3.1) '@sanity/tsdoc': specifier: 1.0.72 - version: 1.0.72(@types/node@18.19.31)(react-dom@18.3.1)(react-is@18.3.1)(react@18.3.1)(sanity@packages+sanity)(styled-components@6.1.11) + version: 1.0.72(@types/node@18.19.31)(debug@4.3.5)(react-dom@18.3.1)(react-is@18.3.1)(react@18.3.1)(sanity@packages+sanity)(styled-components@6.1.11) '@sanity/types': specifier: workspace:* version: link:../../packages/@sanity/types @@ -681,9 +681,6 @@ importers: '@sanity/mutator': specifier: workspace:* version: link:../../@sanity/mutator - '@sanity/portable-text-editor': - specifier: workspace:* - version: link:../../@sanity/portable-text-editor '@sanity/schema': specifier: workspace:* version: link:../../@sanity/schema @@ -1135,127 +1132,6 @@ importers: specifier: ^3.0.2 version: 3.0.2 - packages/@sanity/portable-text-editor: - dependencies: - '@sanity/block-tools': - specifier: 3.48.1 - version: link:../block-tools - '@sanity/schema': - specifier: 3.48.1 - version: link:../schema - '@sanity/types': - specifier: 3.48.1 - version: link:../types - '@sanity/util': - specifier: 3.48.1 - version: link:../util - debug: - specifier: ^3.2.7 - version: 3.2.7 - is-hotkey-esm: - specifier: ^1.0.0 - version: 1.0.0 - lodash: - specifier: ^4.17.21 - version: 4.17.21 - slate: - specifier: 0.100.0 - version: 0.100.0 - slate-react: - specifier: 0.101.0 - version: 0.101.0(react-dom@18.3.1)(react@18.3.1)(slate@0.100.0) - devDependencies: - '@jest/globals': - specifier: ^29.7.0 - version: 29.7.0 - '@playwright/test': - specifier: 1.41.2 - version: 1.41.2 - '@portabletext/toolkit': - specifier: ^2.0.15 - version: 2.0.15 - '@repo/package.config': - specifier: workspace:* - version: link:../../@repo/package.config - '@sanity/diff-match-patch': - specifier: ^3.1.1 - version: 3.1.1 - '@sanity/ui': - specifier: ^2.4.0 - version: 2.4.0(react-dom@18.3.1)(react-is@18.3.1)(react@18.3.1)(styled-components@6.1.11) - '@testing-library/react': - specifier: ^13.4.0 - version: 13.4.0(react-dom@18.3.1)(react@18.3.1) - '@types/debug': - specifier: ^4.1.5 - version: 4.1.12 - '@types/express': - specifier: ^4.17.21 - version: 4.17.21 - '@types/express-ws': - specifier: ^3.0.1 - version: 3.0.4 - '@types/lodash': - specifier: ^4.14.149 - version: 4.17.0 - '@types/node': - specifier: ^18.19.8 - version: 18.19.31 - '@types/node-ipc': - specifier: ^9.2.0 - version: 9.2.3 - '@types/react': - specifier: ^18.3.3 - version: 18.3.3 - '@types/react-dom': - specifier: ^18.3.0 - version: 18.3.0 - '@types/ws': - specifier: ~8.5.3 - version: 8.5.10 - '@vitejs/plugin-react': - specifier: ^4.3.1 - version: 4.3.1(vite@4.5.3) - express: - specifier: ^4.18.3 - version: 4.19.2 - express-ws: - specifier: ^5.0.2 - version: 5.0.2(express@4.19.2) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@18.19.31)(node-notifier@10.0.1) - jest-dev-server: - specifier: ^9.0.1 - version: 9.0.2(debug@3.2.7) - jest-environment-node: - specifier: ^29.7.0 - version: 29.7.0 - node-ipc: - specifier: npm:@node-ipc/compat@9.2.5 - version: /@node-ipc/compat@9.2.5 - react: - specifier: ^18.3.1 - version: 18.3.1 - react-dom: - specifier: ^18.3.1 - version: 18.3.1(react@18.3.1) - rimraf: - specifier: ^3.0.2 - version: 3.0.2 - rxjs: - specifier: ^7.8.1 - version: 7.8.1 - styled-components: - specifier: ^6.1.11 - version: 6.1.11(react-dom@18.3.1)(react@18.3.1) - tsx: - specifier: ^4.10.3 - version: 4.10.5 - vite: - specifier: ^4.5.3 - version: 4.5.3(@types/node@18.19.31) - packages/@sanity/schema: dependencies: '@sanity/generate-help-url': @@ -1309,7 +1185,7 @@ importers: dependencies: '@sanity/client': specifier: ^6.20.0 - version: 6.20.0 + version: 6.20.0(debug@4.3.5) '@types/react': specifier: ^18.0.25 version: 18.3.3 @@ -1331,7 +1207,7 @@ importers: dependencies: '@sanity/client': specifier: ^6.20.0 - version: 6.20.0 + version: 6.20.0(debug@4.3.5) '@sanity/types': specifier: 3.48.1 version: link:../types @@ -1432,7 +1308,7 @@ importers: version: link:../cli '@sanity/client': specifier: ^6.20.0 - version: 6.20.0 + version: 6.20.0(debug@4.3.5) '@sanity/codegen': specifier: workspace:* version: link:../codegen @@ -1445,9 +1321,6 @@ importers: '@sanity/mutator': specifier: workspace:* version: link:../mutator - '@sanity/portable-text-editor': - specifier: workspace:* - version: link:../portable-text-editor '@sanity/schema': specifier: workspace:* version: link:../schema @@ -1502,6 +1375,9 @@ importers: '@juggle/resize-observer': specifier: ^3.3.1 version: 3.4.0 + '@portabletext/editor': + specifier: ^1.0.7 + version: 1.0.7(@sanity/block-tools@packages+@sanity+block-tools)(@sanity/schema@packages+@sanity+schema)(@sanity/types@packages+@sanity+types)(@sanity/util@packages+@sanity+util)(react-dom@18.3.1)(react@18.3.1)(rxjs@7.8.1)(styled-components@6.1.11) '@portabletext/react': specifier: ^3.0.0 version: 3.1.0(react@18.3.1) @@ -1559,9 +1435,6 @@ importers: '@sanity/mutator': specifier: 3.48.1 version: link:../@sanity/mutator - '@sanity/portable-text-editor': - specifier: 3.48.1 - version: link:../@sanity/portable-text-editor '@sanity/presentation': specifier: 1.16.0 version: 1.16.0(@sanity/client@6.20.0)(react-dom@18.3.1)(react-is@18.3.1)(react@18.3.1)(styled-components@6.1.11) @@ -2003,7 +1876,7 @@ importers: version: 1.41.2 '@sanity/client': specifier: ^6.20.0 - version: 6.20.0 + version: 6.20.0(debug@4.3.5) '@sanity/uuid': specifier: ^3.0.1 version: 3.0.2 @@ -4472,16 +4345,6 @@ packages: - supports-color dev: true - /@hapi/hoek@9.3.0: - resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} - dev: true - - /@hapi/topo@5.1.0: - resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} - dependencies: - '@hapi/hoek': 9.3.0 - dev: true - /@humanwhocodes/config-array@0.11.14: resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} @@ -5384,22 +5247,6 @@ packages: eslint-scope: 5.1.1 dev: true - /@node-ipc/compat@9.2.5: - resolution: {integrity: sha512-JdbaM9jeMi3JZLS7z5SDcrwFwind0JMA/MaEaP8NzxHZSxWhhCc22JN9pioPUsy3Ijy6wQA6DoRVpW1LoR6/FA==} - engines: {node: '>=4.0.0'} - dependencies: - '@node-ipc/js-queue': 2.0.3 - event-pubsub: 4.2.3 - js-message: 1.0.5 - dev: true - - /@node-ipc/js-queue@2.0.3: - resolution: {integrity: sha512-fL1wpr8hhD5gT2dA1qifeVaoDFlQR5es8tFuKqjHX+kdOtdNHnxkVZbtIrR2rxnMFvehkjaZRNV2H/gPXlb0hw==} - engines: {node: '>=1.0.0'} - dependencies: - easy-stack: 1.0.1 - dev: true - /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -5845,6 +5692,43 @@ packages: '@pnpm/network.ca-file': 1.0.2 config-chain: 1.1.13 + /@portabletext/editor@1.0.7(@sanity/block-tools@packages+@sanity+block-tools)(@sanity/schema@packages+@sanity+schema)(@sanity/types@packages+@sanity+types)(@sanity/util@packages+@sanity+util)(react-dom@18.3.1)(react@18.3.1)(rxjs@7.8.1)(styled-components@6.1.11): + resolution: {integrity: sha512-IJnw3a8coYHKtS2RsxLzFlqkfQrLCpD+aggSLmZLy3xGLoEOTCybpFmSvrRDCD43ApBKfWtnD9zTfRoIuM2wmQ==} + engines: {node: '>=18'} + peerDependencies: + '@sanity/block-tools': ^3.47.1 + '@sanity/schema': ^3.47.1 + '@sanity/types': ^3.47.1 + '@sanity/util': ^3.47.1 + react: '*' + rxjs: ^7 + styled-components: ^6.1 + dependencies: + '@portabletext/patches': 1.0.2 + '@sanity/block-tools': link:packages/@sanity/block-tools + '@sanity/schema': link:packages/@sanity/schema + '@sanity/types': link:packages/@sanity/types + '@sanity/util': link:packages/@sanity/util + debug: 4.3.5(supports-color@9.4.0) + is-hotkey-esm: 1.0.0 + lodash: 4.17.21 + react: 18.3.1 + rxjs: 7.8.1 + slate: 0.100.0 + slate-react: 0.101.0(react-dom@18.3.1)(react@18.3.1)(slate@0.100.0) + styled-components: 6.1.11(react-dom@18.3.1)(react@18.3.1) + transitivePeerDependencies: + - react-dom + - supports-color + dev: false + + /@portabletext/patches@1.0.2: + resolution: {integrity: sha512-vRENK7hwja/gHOtqvGKY9SewrPRnLnJy7CX2dIQQnUkL4GZmUQ/mSYjNBVDJqk30JbYpZPsxhkftjzK/g3BkRA==} + dependencies: + '@sanity/diff-match-patch': 3.1.1 + lodash: 4.17.21 + dev: false + /@portabletext/react@3.1.0(react@18.3.1): resolution: {integrity: sha512-ZGHlvS+NvId9RSqnflN8xF2KVZgAgD399dK1GaycurnGNZGZYTd5nZmc8by1yL76Ar8n/dbVtouUDJIkO4Tupw==} engines: {node: ^14.13.1 || >=16.0.0} @@ -6734,16 +6618,6 @@ packages: /@sanity/browserslist-config@1.0.3: resolution: {integrity: sha512-UkJuiTyROgPcxbvpHYyXwr+T88Np4eLzu3h05gMgeZ2hv3EM7g/4VMyng5HuA1JdPQPEdq8bmmfQDR+u4KC+TA==} - /@sanity/client@6.20.0: - resolution: {integrity: sha512-p6ENOFBR1QQirKj6zVYPDeZNVHMVV+3UVo+RptZDQPuyYyfAOA/TwJzX3cE19O2G4CgWnLfkmOOg6RcFjzFMWQ==} - engines: {node: '>=14.18'} - dependencies: - '@sanity/eventsource': 5.0.2 - get-it: 8.6.0 - rxjs: 7.8.1 - transitivePeerDependencies: - - debug - /@sanity/client@6.20.0(debug@4.3.5): resolution: {integrity: sha512-p6ENOFBR1QQirKj6zVYPDeZNVHMVV+3UVo+RptZDQPuyYyfAOA/TwJzX3cE19O2G4CgWnLfkmOOg6RcFjzFMWQ==} engines: {node: '>=14.18'} @@ -6764,12 +6638,13 @@ packages: peerDependencies: '@sanity/client': ^6.19.1 dependencies: - '@sanity/client': 6.20.0 + '@sanity/client': 6.20.0(debug@4.3.5) dev: false /@sanity/diff-match-patch@3.1.1: resolution: {integrity: sha512-dSZqGeYjHKGIkqAzGqLcG92LZyJGX+nYbs/FWawhBbTBDWi21kvQ0hsL3DJThuFVWtZMWTQijN3z6Cnd44Pf2g==} engines: {node: '>=14.18'} + dev: false /@sanity/eslint-config-i18n@1.0.0(eslint@8.57.0)(typescript@5.4.5): resolution: {integrity: sha512-BIeD9IVT7O5I6vDyDaICoidN02qeImdXDRAW062iHY9gV4JrGScWBFio2HQLso7C+Z6SrQB8jOft6SzeYqDhdQ==} @@ -7067,63 +6942,6 @@ packages: - '@types/node' - debug - supports-color - dev: true - - /@sanity/pkg-utils@6.9.3(@types/node@18.19.31)(typescript@5.4.5): - resolution: {integrity: sha512-AydMp57nHCVA2AYx2czLsdIan7EN1XVI/t/EDLDT6lPJKs4JDQOc7hkc8DcYMikNr6y6ryOfLbWeHjXVddRBKQ==} - engines: {node: '>=18.17.0'} - hasBin: true - peerDependencies: - typescript: 5.4.x - dependencies: - '@babel/core': 7.24.7 - '@babel/preset-typescript': 7.24.7(@babel/core@7.24.7) - '@babel/types': 7.24.7 - '@microsoft/api-extractor': 7.47.0(@types/node@18.19.31) - '@microsoft/tsdoc-config': 0.17.0 - '@optimize-lodash/rollup-plugin': 4.0.4(rollup@4.18.0) - '@rollup/plugin-alias': 5.1.0(rollup@4.18.0) - '@rollup/plugin-babel': 6.0.4(@babel/core@7.24.7)(rollup@4.18.0) - '@rollup/plugin-commonjs': 26.0.1(rollup@4.18.0) - '@rollup/plugin-json': 6.1.0(rollup@4.18.0) - '@rollup/plugin-node-resolve': 15.2.3(rollup@4.18.0) - '@rollup/plugin-replace': 5.0.7(rollup@4.18.0) - '@rollup/plugin-terser': 0.4.4(rollup@4.18.0) - '@sanity/browserslist-config': 1.0.3 - babel-plugin-react-compiler: 0.0.0-experimental-938cd9a-20240601 - browserslist: 4.23.1 - cac: 6.7.14 - chalk: 4.1.2 - chokidar: 3.6.0 - esbuild: 0.21.5 - esbuild-register: 3.5.0(esbuild@0.21.5) - find-config: 1.0.0 - get-latest-version: 5.1.0 - git-url-parse: 14.0.0 - globby: 11.1.0 - jsonc-parser: 3.2.1 - mkdirp: 3.0.1 - outdent: 0.8.0 - parse-git-config: 3.0.0 - pkg-up: 3.1.0 - prettier: 3.3.2 - pretty-bytes: 5.6.0 - prompts: 2.4.2 - recast: 0.23.9 - rimraf: 4.4.1 - rollup: 4.18.0 - rollup-plugin-esbuild: 6.1.1(esbuild@0.21.5)(rollup@4.18.0) - rxjs: 7.8.1 - treeify: 1.1.0 - typescript: 5.4.5 - uuid: 9.0.1 - zod: 3.23.8 - zod-validation-error: 3.3.0(zod@3.23.8) - transitivePeerDependencies: - - '@types/babel__core' - - '@types/node' - - debug - - supports-color /@sanity/presentation@1.16.0(@sanity/client@6.20.0)(react-dom@18.3.1)(react-is@18.3.1)(react@18.3.1)(styled-components@6.1.11): resolution: {integrity: sha512-8nNGPM+r+D8dRe/UVcDEO6Z9gzS5LcOIQMzziOg8nMUGz284pcuEIzvRI9XQ3gbMiv6Zyo+fzuJPktoq+dkqhw==} @@ -7168,7 +6986,7 @@ packages: peerDependencies: '@sanity/client': ^6.19.1 dependencies: - '@sanity/client': 6.20.0 + '@sanity/client': 6.20.0(debug@4.3.5) '@sanity/uuid': 3.0.2 dev: false @@ -7179,7 +6997,7 @@ packages: '@sanity/client': ^6.19.1 react: '*' dependencies: - '@sanity/client': 6.20.0 + '@sanity/client': 6.20.0(debug@4.3.5) '@sanity/core-loader': 1.6.19(@sanity/client@6.20.0) react: 18.3.1 dev: false @@ -7201,7 +7019,7 @@ packages: hasBin: true dependencies: '@playwright/test': 1.41.2 - '@sanity/client': 6.20.0 + '@sanity/client': 6.20.0(debug@4.3.5) '@sanity/uuid': 3.0.2 cac: 6.7.14 transitivePeerDependencies: @@ -7267,67 +7085,6 @@ packages: - sugarss - supports-color - terser - dev: true - - /@sanity/tsdoc@1.0.72(@types/node@18.19.31)(react-dom@18.3.1)(react-is@18.3.1)(react@18.3.1)(sanity@packages+sanity)(styled-components@6.1.11): - resolution: {integrity: sha512-ZaEXt83nsyIaeJndrSDUnujLiHx/s3l85l6D7GAWTjG61adkZeELtV+TINBU2DmN7X04V4335MNuuZjVSCIRdg==} - engines: {node: '>=14.0.0'} - hasBin: true - peerDependencies: - react: '*' - react-dom: '*' - sanity: ^3 - styled-components: ^5.2 || ^6 - dependencies: - '@microsoft/api-extractor': 7.47.0(@types/node@18.19.31) - '@microsoft/api-extractor-model': 7.29.2(@types/node@18.19.31) - '@microsoft/tsdoc': 0.15.0 - '@microsoft/tsdoc-config': 0.17.0 - '@portabletext/react': 3.1.0(react@18.3.1) - '@portabletext/toolkit': 2.0.15 - '@sanity/client': 6.20.0 - '@sanity/color': 3.0.6 - '@sanity/icons': 3.2.0(react@18.3.1) - '@sanity/pkg-utils': 6.9.3(@types/node@18.19.31)(typescript@5.4.5) - '@sanity/ui': 2.4.0(react-dom@18.3.1)(react-is@18.3.1)(react@18.3.1)(styled-components@6.1.11) - '@types/cpx': 1.5.5 - '@vitejs/plugin-react': 4.3.1(vite@5.2.13) - cac: 6.7.14 - chalk: 4.1.2 - chokidar: 3.6.0 - cors: 2.8.5 - dotenv-flow: 3.3.0 - esbuild: 0.21.5 - esbuild-register: 3.5.0(esbuild@0.21.5) - express: 4.19.2 - globby: 11.1.0 - groq: 3.48.0 - groq-js: 1.10.0 - history: 5.3.0 - jsonc-parser: 3.2.1 - mkdirp: 1.0.4 - pkg-up: 3.1.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-refractor: 2.2.0(react@18.3.1) - sanity: link:packages/sanity - slugify: 1.6.6 - styled-components: 6.1.11(react-dom@18.3.1)(react@18.3.1) - tmp: 0.2.3 - typescript: 5.4.5 - vite: 5.2.13(@types/node@18.19.31) - transitivePeerDependencies: - - '@types/babel__core' - - '@types/node' - - debug - - less - - lightningcss - - react-is - - sass - - stylus - - sugarss - - supports-color - - terser /@sanity/types@3.37.2(debug@4.3.5): resolution: {integrity: sha512-1EfKkNlJ86wIDtc7oFHb79JI8lKDOxKDYrkmwhvuHgJY83GpSABc1kFdbwAtWZfrWVWyqVXUv/KlNwA3b99y/g==} @@ -7341,7 +7098,7 @@ packages: /@sanity/types@3.48.1: resolution: {integrity: sha512-UG+AjRPYhh+URH5pBrIQ4h81rRbVZ+J/WLL+vP9uL/bseq61etWIYz8iljXWuReVHbqBPLGHQF1EpcMX1EZ5MQ==} dependencies: - '@sanity/client': 6.20.0 + '@sanity/client': 6.20.0(debug@4.3.5) '@types/react': 18.3.3 transitivePeerDependencies: - debug @@ -7423,7 +7180,7 @@ packages: resolution: {integrity: sha512-MTWKGuE88ASGnx9nngqAd0ZphVXppCIIgh5KB/xvMDigaWcrP5tWW34XR6yN52/6kRHGxU2ehyC7RRZDMTj9pQ==} engines: {node: '>=18'} dependencies: - '@sanity/client': 6.20.0 + '@sanity/client': 6.20.0(debug@4.3.5) '@sanity/types': 3.48.1 get-random-values-esm: 1.0.2 moment: 2.30.1 @@ -7461,7 +7218,7 @@ packages: svelte: optional: true dependencies: - '@sanity/client': 6.20.0 + '@sanity/client': 6.20.0(debug@4.3.5) '@sanity/preview-url-secret': 1.6.17(@sanity/client@6.20.0) '@vercel/stega': 0.1.2 react: 18.3.1 @@ -7560,20 +7317,6 @@ packages: '@sentry/types': 8.9.2 dev: false - /@sideway/address@4.1.5: - resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==} - dependencies: - '@hapi/hoek': 9.3.0 - dev: true - - /@sideway/formula@3.0.1: - resolution: {integrity: sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==} - dev: true - - /@sideway/pinpoint@2.0.0: - resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} - dev: true - /@sigstore/bundle@1.1.0: resolution: {integrity: sha512-PFutXEy0SmQxYI4texPw3dd2KewuNqv7OuK1ZFtY2fM754yhvG2KdgwIhRnoEE2uHdtdGNQ8s0lb94dW9sELog==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -7952,13 +7695,6 @@ packages: dependencies: '@babel/types': 7.24.7 - /@types/body-parser@1.19.5: - resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} - dependencies: - '@types/connect': 3.4.38 - '@types/node': 18.19.31 - dev: true - /@types/caseless@0.12.5: resolution: {integrity: sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==} dev: true @@ -7974,12 +7710,6 @@ packages: '@types/node': 18.19.31 dev: true - /@types/connect@3.4.38: - resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} - dependencies: - '@types/node': 18.19.31 - dev: true - /@types/cpx@1.5.5: resolution: {integrity: sha512-PwM+cN40GZcjG9YgGFp/rQGKOpTqr6scUl1Q85NHL5jieh9I203kKiArjJcExwxy4+vTABmVUNRkNvGbPnRQZg==} dependencies: @@ -8019,23 +7749,6 @@ packages: '@types/send': 0.17.4 dev: true - /@types/express-ws@3.0.4: - resolution: {integrity: sha512-Yjj18CaivG5KndgcvzttWe8mPFinPCHJC2wvyQqVzA7hqeufM8EtWMj6mpp5omg3s8XALUexhOu8aXAyi/DyJQ==} - dependencies: - '@types/express': 4.17.21 - '@types/express-serve-static-core': 4.19.0 - '@types/ws': 8.5.10 - dev: true - - /@types/express@4.17.21: - resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} - dependencies: - '@types/body-parser': 1.19.5 - '@types/express-serve-static-core': 4.19.0 - '@types/qs': 6.9.14 - '@types/serve-static': 1.15.7 - dev: true - /@types/glob@7.2.0: resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} dependencies: @@ -8053,10 +7766,6 @@ packages: dependencies: '@types/unist': 2.0.10 - /@types/http-errors@2.0.4: - resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} - dev: true - /@types/inquirer@6.5.0: resolution: {integrity: sha512-rjaYQ9b9y/VFGOpqBEXRavc3jh0a+e6evAbI31tMda8VlPaSy0AZJfXsvmIe3wklc7W6C3zCSfleuMXR7NOyXw==} dependencies: @@ -8144,12 +7853,6 @@ packages: resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} dev: true - /@types/node-ipc@9.2.3: - resolution: {integrity: sha512-/MvSiF71fYf3+zwqkh/zkVkZj1hl1Uobre9EMFy08mqfJNAmpR0vmPgOUdEIDVgifxHj6G1vYMPLSBLLxoDACQ==} - dependencies: - '@types/node': 18.19.31 - dev: true - /@types/node@18.19.31: resolution: {integrity: sha512-ArgCD39YpyyrtFKIqMDvjz79jto5fcI/SVUs2HwB+f0dAzq68yqOdyaSivLiLugSziTpNXLQrVb7RZFmdZzbhA==} dependencies: @@ -8279,14 +7982,6 @@ packages: '@types/node': 18.19.31 dev: true - /@types/serve-static@1.15.7: - resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} - dependencies: - '@types/http-errors': 2.0.4 - '@types/node': 18.19.31 - '@types/send': 0.17.4 - dev: true - /@types/shallow-equals@1.0.3: resolution: {integrity: sha512-xZx/hZsf1p9J5lGN/nGTsuW/chJCdlyGxilwg1TS78rygBCU5bpY50zZiFcIimlnl0p41kAyaASsy0bqU7WyBA==} dev: false @@ -8377,12 +8072,6 @@ packages: resolution: {integrity: sha512-113D3mDkZDjo+EeUEHCFy0qniNc1ZpecGiAU7WSo7YDoSzolZIQKpYFHrPpjkB2nuyahcKfrmLXeQlh7gqJYdw==} dev: true - /@types/ws@8.5.10: - resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} - dependencies: - '@types/node': 18.19.31 - dev: true - /@types/yargs-parser@21.0.3: resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -9215,10 +8904,10 @@ packages: resolution: {integrity: sha512-H5orY+M2Fr56DWmMFpMrq5Ge93qjNdPVqzBv5gWK3aD1OvjBEJlEzxf09z93dGVQeI0LiW+aCMIx1QtShC/zUw==} engines: {node: '>=4'} - /axios@1.6.8(debug@3.2.7): + /axios@1.6.8: resolution: {integrity: sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==} dependencies: - follow-redirects: 1.15.6(debug@3.2.7) + follow-redirects: 1.15.6(debug@4.3.5) form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -10102,11 +9791,6 @@ packages: /commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - /commander@5.1.0: - resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} - engines: {node: '>= 6'} - dev: true - /commander@9.5.0: resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} engines: {node: ^12.20.0 || >=14} @@ -10511,14 +10195,6 @@ packages: resolution: {integrity: sha512-nDyMobZgoAVqz7mA8rsn7i1/6bjH6N9ab2Ge7LyyNxrvxAq7zQJPg8i3u2VH7wEB+Y1T1+C3/h1G774/D+ZLag==} dev: false - /cwd@0.10.0: - resolution: {integrity: sha512-YGZxdTTL9lmLkCUTpg4j0zQ7IhRB5ZmqNBbGCl3Tg6MP/d5/6sY7L5mmTjzbc6JKgVZYiqTQTNhPFsbXNGlRaA==} - engines: {node: '>=0.8'} - dependencies: - find-pkg: 0.1.2 - fs-exists-sync: 0.1.0 - dev: true - /cyclist@1.0.2: resolution: {integrity: sha512-0sVXIohTfLqVIW3kb/0n6IiWF3Ifj5nm2XaSrLq2DI6fKIGa2fYAZdk917rUneaeLVpYfFcyXE2ft0fe3remsA==} dev: false @@ -10638,6 +10314,7 @@ packages: optional: true dependencies: ms: 2.1.3 + dev: true /debug@4.3.5(supports-color@5.5.0): resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==} @@ -11110,11 +10787,6 @@ packages: /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - /easy-stack@1.0.1: - resolution: {integrity: sha512-wK2sCs4feiiJeFXn3zvY0p41mdU5VUgbgs1rNsc/y5ngFUijdWd+iIN8eoyuZHKB8xN6BL4PdWmzqFmxNg6V2w==} - engines: {node: '>=6.0.0'} - dev: true - /ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} dependencies: @@ -11550,13 +11222,13 @@ packages: eslint-plugin-simple-import-sort: 12.1.0(eslint@8.57.0) dev: true - /eslint-config-turbo@2.0.4(eslint@8.57.0): - resolution: {integrity: sha512-zGvU+bxoNWVvSl0prGItrnH9FgeNzKEAjRmv8ruqql1psI37T8IoLF/XeOzT3CzzYzJxuI3wW1yb2agDFYQdHQ==} + /eslint-config-turbo@2.0.6(eslint@8.57.0): + resolution: {integrity: sha512-PkRjFnZUZWPcrYT4Xoi5OWOUtnn6xVGh88I6TsayiH4AQZuLs/MDmzfJRK+PiWIrI7Q7sbsVEQP+nUyyRE3uAw==} peerDependencies: eslint: '>6.6.0' dependencies: eslint: 8.57.0 - eslint-plugin-turbo: 2.0.4(eslint@8.57.0) + eslint-plugin-turbo: 2.0.6(eslint@8.57.0) dev: true /eslint-import-resolver-node@0.3.9: @@ -11798,8 +11470,8 @@ packages: '@microsoft/tsdoc-config': 0.17.0 dev: true - /eslint-plugin-turbo@2.0.4(eslint@8.57.0): - resolution: {integrity: sha512-Ozn//vTXJeqIEvEkThM2vuuldMckPqAne7vg/S3GxF+BBY516cjdp7+dYpCU5Q0083hVm638c8542ubccNE+8w==} + /eslint-plugin-turbo@2.0.6(eslint@8.57.0): + resolution: {integrity: sha512-yGnpMvyBxI09ZrF5bGpaniBz57MiExTCsRnNxP+JnbMFD+xU3jG3ukRzehVol8LYNdC/G7E4HoH+x7OEpoSGAQ==} peerDependencies: eslint: '>6.6.0' dependencies: @@ -11976,11 +11648,6 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} - /event-pubsub@4.2.3: - resolution: {integrity: sha512-H/MISqO/4Ud8R9wZUlBpz/I+WZL9qUwV6/jhiu6QEEqwEphJMCVjGgut65zpsgbR/2+qcQyF/SfKpaFXR4/aaA==} - engines: {node: '>=4.0.0'} - dev: true - /event-source-polyfill@1.0.31: resolution: {integrity: sha512-4IJSItgS/41IxN5UVAVuAyczwZF7ZIEsM1XAoUzIHA6A+xzusEZUutdXz2Nr+MQPLxfTiCvqE79/C8HT8fKFvA==} @@ -12088,13 +11755,6 @@ packages: fill-range: 2.2.4 dev: true - /expand-tilde@1.2.2: - resolution: {integrity: sha512-rtmc+cjLZqnu9dSYosX9EWmSJhTwpACgJQTfj4hgg2JjOD/6SIQalZrt4a3aQeh++oNxkazcaxrhPUj6+g5G/Q==} - engines: {node: '>=0.10.0'} - dependencies: - os-homedir: 1.0.2 - dev: true - /expand-tilde@2.0.2: resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==} engines: {node: '>=0.10.0'} @@ -12117,19 +11777,6 @@ packages: resolution: {integrity: sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==} dev: true - /express-ws@5.0.2(express@4.19.2): - resolution: {integrity: sha512-0uvmuk61O9HXgLhGl3QhNSEtRsQevtmbL94/eILaliEADZBHZOQUAiHFrGPrgsjikohyrmSG5g+sCfASTt0lkQ==} - engines: {node: '>=4.5.0'} - peerDependencies: - express: ^4.0.0 || ^5.0.0-alpha.1 - dependencies: - express: 4.19.2 - ws: 7.5.9 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - dev: true - /express@4.19.2: resolution: {integrity: sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==} engines: {node: '>= 0.10.0'} @@ -12395,36 +12042,10 @@ packages: dependencies: user-home: 2.0.0 - /find-file-up@0.1.3: - resolution: {integrity: sha512-mBxmNbVyjg1LQIIpgO8hN+ybWBgDQK8qjht+EbrTCGmmPV/sc7RF1i9stPTD6bpvXZywBdrwRYxhSdJv867L6A==} - engines: {node: '>=0.10.0'} - dependencies: - fs-exists-sync: 0.1.0 - resolve-dir: 0.1.1 - dev: true - /find-index@0.1.1: resolution: {integrity: sha512-uJ5vWrfBKMcE6y2Z8834dwEZj9mNGxYa3t3I53OwFeuZ8D9oc2E5zcsrkuhX6h4iYrjhiv0T3szQmxlAV9uxDg==} dev: true - /find-pkg@0.1.2: - resolution: {integrity: sha512-0rnQWcFwZr7eO0513HahrWafsc3CTFioEB7DRiEYCUM/70QXSY8f3mCST17HXLcPvEhzH/Ty/Bxd72ZZsr/yvw==} - engines: {node: '>=0.10.0'} - dependencies: - find-file-up: 0.1.3 - dev: true - - /find-process@1.4.7: - resolution: {integrity: sha512-/U4CYp1214Xrp3u3Fqr9yNynUrr5Le4y0SsJh2lMDDSbpwYSz3M2SMWQC+wqcx79cN8PQtHQIL8KnuY9M66fdg==} - hasBin: true - dependencies: - chalk: 4.1.2 - commander: 5.1.0 - debug: 4.3.5(supports-color@9.4.0) - transitivePeerDependencies: - - supports-color - dev: true - /find-up@2.1.0: resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==} engines: {node: '>=4'} @@ -12501,17 +12122,6 @@ packages: tslib: 2.6.3 dev: false - /follow-redirects@1.15.6(debug@3.2.7): - resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - dependencies: - debug: 3.2.7 - /follow-redirects@1.15.6(debug@4.3.5): resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} engines: {node: '>=4.0'} @@ -12754,18 +12364,6 @@ packages: has-symbols: 1.0.3 hasown: 2.0.2 - /get-it@8.6.0: - resolution: {integrity: sha512-ZFNZc3eKkYRHI5a7p3SAc3s0eBJgLL+qqpF7wpoFbTdzbHKC/XHu+6ot9RZTe6aoYGmZqf3Mdl62XdgiWJ7/ZQ==} - engines: {node: '>=14.0.0'} - dependencies: - decompress-response: 7.0.0 - follow-redirects: 1.15.6(debug@3.2.7) - is-retry-allowed: 2.2.0 - progress-stream: 2.0.0 - tunnel-agent: 0.6.0 - transitivePeerDependencies: - - debug - /get-it@8.6.0(debug@4.3.5): resolution: {integrity: sha512-ZFNZc3eKkYRHI5a7p3SAc3s0eBJgLL+qqpF7wpoFbTdzbHKC/XHu+6ot9RZTe6aoYGmZqf3Mdl62XdgiWJ7/ZQ==} engines: {node: '>=14.0.0'} @@ -12778,17 +12376,6 @@ packages: transitivePeerDependencies: - debug - /get-latest-version@5.1.0: - resolution: {integrity: sha512-Q6IBWr/zzw57zIkJmNhI23eRTw3nZ4BWWK034meLwOYU9L3J3IpXiyM73u2pYUwN6U7ahkerCwg2T0jlxiLwsw==} - engines: {node: '>=14.18'} - dependencies: - get-it: 8.6.0 - registry-auth-token: 5.0.2 - registry-url: 5.1.0 - semver: 7.6.2 - transitivePeerDependencies: - - debug - /get-latest-version@5.1.0(debug@4.3.5): resolution: {integrity: sha512-Q6IBWr/zzw57zIkJmNhI23eRTw3nZ4BWWK034meLwOYU9L3J3IpXiyM73u2pYUwN6U7ahkerCwg2T0jlxiLwsw==} engines: {node: '>=14.18'} @@ -12799,7 +12386,6 @@ packages: semver: 7.6.2 transitivePeerDependencies: - debug - dev: true /get-package-type@0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} @@ -13066,14 +12652,6 @@ packages: ini: 2.0.0 dev: true - /global-modules@0.2.3: - resolution: {integrity: sha512-JeXuCbvYzYXcwE6acL9V2bAOeSIGl4dD+iwLY9iUx2VBJJ80R18HCn+JCwHM9Oegdfya3lEkGCdaRkSyc10hDA==} - engines: {node: '>=0.10.0'} - dependencies: - global-prefix: 0.1.5 - is-windows: 0.2.0 - dev: true - /global-modules@1.0.0: resolution: {integrity: sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==} engines: {node: '>=0.10.0'} @@ -13083,16 +12661,6 @@ packages: resolve-dir: 1.0.1 dev: true - /global-prefix@0.1.5: - resolution: {integrity: sha512-gOPiyxcD9dJGCEArAhF4Hd0BAqvAe/JzERP7tYumE4yIkmIedPUVXcJFWbV3/p/ovIIvKjkrTk+f1UVkq7vvbw==} - engines: {node: '>=0.10.0'} - dependencies: - homedir-polyfill: 1.0.3 - ini: 1.3.8 - is-windows: 0.2.0 - which: 1.3.1 - dev: true - /global-prefix@1.0.2: resolution: {integrity: sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==} engines: {node: '>=0.10.0'} @@ -14201,11 +13769,6 @@ packages: get-intrinsic: 1.2.4 dev: true - /is-windows@0.2.0: - resolution: {integrity: sha512-n67eJYmXbniZB7RF4I/FTjK1s6RPOCTxhYrVYLRaCt3lF0mpWZPKr3T2LSZAqyjQsxR2qMmGYXXzK0YWwcPM1Q==} - engines: {node: '>=0.10.0'} - dev: true - /is-windows@1.0.2: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} @@ -14456,22 +14019,6 @@ packages: - supports-color dev: true - /jest-dev-server@9.0.2(debug@3.2.7): - resolution: {integrity: sha512-Zc/JB0IlNNrpXkhBw+h86cGrde/Mey52KvF+FER2eyrtYJTHObOwW7Iarxm3rPyTKby5+3Y2QZtl8pRz/5GCxg==} - engines: {node: '>=16'} - dependencies: - chalk: 4.1.2 - cwd: 0.10.0 - find-process: 1.4.7 - prompts: 2.4.2 - spawnd: 9.0.2 - tree-kill: 1.2.2 - wait-on: 7.2.0(debug@3.2.7) - transitivePeerDependencies: - - debug - - supports-color - dev: true - /jest-diff@29.7.0: resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -14803,21 +14350,6 @@ packages: /jju@1.4.0: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} - /joi@17.12.3: - resolution: {integrity: sha512-2RRziagf555owrm9IRVtdKynOBeITiDpuZqIpgwqXShPncPKNiRQoiGsl/T8SQdq+8ugRzH2LqY67irr2y/d+g==} - dependencies: - '@hapi/hoek': 9.3.0 - '@hapi/topo': 5.1.0 - '@sideway/address': 4.1.5 - '@sideway/formula': 3.0.1 - '@sideway/pinpoint': 2.0.0 - dev: true - - /js-message@1.0.5: - resolution: {integrity: sha512-hTqHqrm7jrZ+iN93QsKcNOTSgX3F+2NSgdnF+xvf8FfhC2MPqYRzzgXQ1LlhfyIzPTS6hL6Zea0/gIb6hktkHw==} - engines: {node: '>=0.6.0'} - dev: true - /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -16563,7 +16095,7 @@ packages: '@yarnpkg/lockfile': 1.1.0 '@yarnpkg/parsers': 3.0.0-rc.46 '@zkochan/js-yaml': 0.0.6 - axios: 1.6.8(debug@3.2.7) + axios: 1.6.8 chalk: 4.1.2 cli-cursor: 3.1.0 cli-spinners: 2.6.1 @@ -18170,14 +17702,6 @@ packages: resolve-from: 5.0.0 dev: true - /resolve-dir@0.1.1: - resolution: {integrity: sha512-QxMPqI6le2u0dCLyiGzgy92kjkkL6zO0XyvHzjdTNH3zM6e5Hz3BwG6+aEyNgiQ5Xz6PwTwgQEj3U50dByPKIA==} - engines: {node: '>=0.10.0'} - dependencies: - expand-tilde: 1.2.2 - global-modules: 0.2.3 - dev: true - /resolve-dir@1.0.1: resolution: {integrity: sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==} engines: {node: '>=0.10.0'} @@ -18988,14 +18512,6 @@ packages: /space-separated-tokens@1.1.5: resolution: {integrity: sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==} - /spawnd@9.0.2: - resolution: {integrity: sha512-nl8DVHEDQ57IcKakzpjanspVChkMpGLuVwMR/eOn9cXE55Qr6luD2Kn06sA0ootRMdgrU4tInN6lA6ohTNvysw==} - engines: {node: '>=16'} - dependencies: - signal-exit: 4.1.0 - tree-kill: 1.2.2 - dev: true - /spdx-correct@3.2.0: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} dependencies: @@ -19810,11 +19326,6 @@ packages: dependencies: punycode: 2.3.1 - /tree-kill@1.2.2: - resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} - hasBin: true - dev: true - /treeify@1.1.0: resolution: {integrity: sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==} engines: {node: '>=0.6'} @@ -19931,17 +19442,6 @@ packages: /tslib@2.6.3: resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} - /tsx@4.10.5: - resolution: {integrity: sha512-twDSbf7Gtea4I2copqovUiNTEDrT8XNFXsuHpfGbdpW/z9ZW4fTghzzhAG0WfrCuJmJiOEY1nLIjq4u3oujRWQ==} - engines: {node: '>=18.0.0'} - hasBin: true - dependencies: - esbuild: 0.20.2 - get-tsconfig: 4.7.5 - optionalDependencies: - fsevents: 2.3.3 - dev: true - /tuf-js@1.1.7: resolution: {integrity: sha512-i3P9Kgw3ytjELUfpuKVDNBJvk4u5bXL6gskv572mcevPbSKCV3zt3djhmlEQ65yERjIbOSncy7U4cQJaB1CBCg==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -19984,64 +19484,64 @@ packages: engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} dev: true - /turbo-darwin-64@2.0.4: - resolution: {integrity: sha512-x9mvmh4wudBstML8Z8IOmokLWglIhSfhQwnh2gBCSqabgVBKYvzl8Y+i+UCNPxheCGTgtsPepTcIaKBIyFIcvw==} + /turbo-darwin-64@2.0.6: + resolution: {integrity: sha512-XpgBwWj3Ggmz/gQVqXdMKXHC1iFPMDiuwugLwSzE7Ih0O13JuNtYZKhQnopvbDQnFQCeRq2Vsm5OTWabg/oB/g==} cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-darwin-arm64@2.0.4: - resolution: {integrity: sha512-/B1Ih8zPRGVw5vw4SlclOf3C/woJ/2T6ieH6u54KT4wypoaVyaiyMqBcziIXycdObIYr7jQ+raHO7q3mhay9/A==} + /turbo-darwin-arm64@2.0.6: + resolution: {integrity: sha512-RfeZYXIAkiA21E8lsvfptGTqz/256YD+eI1x37fedfvnHFWuIMFZGAOwJxtZc6QasQunDZ9TRRREbJNI68tkIw==} cpu: [arm64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-linux-64@2.0.4: - resolution: {integrity: sha512-6aG670e5zOWu6RczEYcB81nEl8EhiGJEvWhUrnAfNEUIMBEH1pR5SsMmG2ol5/m3PgiRM12r13dSqTxCLcHrVg==} + /turbo-linux-64@2.0.6: + resolution: {integrity: sha512-92UDa0xNQQbx0HdSp9ag3YSS3xPdavhc7q9q9mxIAcqyjjD6VElA4Y85m4F/DDGE5SolCrvBz2sQhVmkOd6Caw==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-linux-arm64@2.0.4: - resolution: {integrity: sha512-AXfVOjst+mCtPDFT4tCu08Qrfv12Nj7NDd33AjGwV79NYN1Y1rcFY59UQ4nO3ij3rbcvV71Xc+TZJ4csEvRCSg==} + /turbo-linux-arm64@2.0.6: + resolution: {integrity: sha512-eQKu6utCVUkIH2kqOzD8OS6E0ba6COjWm6PRDTNCHQRljZW503ycaTUIdMOiJrVg1MkEjDyOReUg8s8D18aJ4Q==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-windows-64@2.0.4: - resolution: {integrity: sha512-QOnUR9hKl0T5gq5h1fAhVEqBSjpcBi/BbaO71YGQNgsr6pAnCQdbG8/r3MYXet53efM0KTdOhieWeO3KLNKybA==} + /turbo-windows-64@2.0.6: + resolution: {integrity: sha512-+9u4EPrpoeHYCQ46dRcou9kbkSoelhOelHNcbs2d86D6ruYD/oIAHK9qgYK8LeARRz0jxhZIA/dWYdYsxJJWkw==} cpu: [x64] os: [win32] requiresBuild: true dev: true optional: true - /turbo-windows-arm64@2.0.4: - resolution: {integrity: sha512-3v8WpdZy1AxZw0gha0q3caZmm+0gveBQ40OspD6mxDBIS+oBtO5CkxhIXkFJJW+jDKmDlM7wXDIGfMEq+QyNCQ==} + /turbo-windows-arm64@2.0.6: + resolution: {integrity: sha512-rdrKL+p+EjtdDVg0wQ/7yTbzkIYrnb0Pw4IKcjsy3M0RqUM9UcEi67b94XOAyTa5a0GqJL1+tUj2ebsFGPgZbg==} cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /turbo@2.0.4: - resolution: {integrity: sha512-Ilme/2Q5kYw0AeRr+aw3s02+WrEYaY7U8vPnqSZU/jaDG/qd6jHVN6nRWyd/9KXvJGYM69vE6JImoGoyNjLwaw==} + /turbo@2.0.6: + resolution: {integrity: sha512-/Ftmxd5Mq//a9yMonvmwENNUN65jOVTwhhBPQjEtNZutYT9YKyzydFGLyVM1nzhpLWahQSMamRc/RDBv5EapzA==} hasBin: true optionalDependencies: - turbo-darwin-64: 2.0.4 - turbo-darwin-arm64: 2.0.4 - turbo-linux-64: 2.0.4 - turbo-linux-arm64: 2.0.4 - turbo-windows-64: 2.0.4 - turbo-windows-arm64: 2.0.4 + turbo-darwin-64: 2.0.6 + turbo-darwin-arm64: 2.0.6 + turbo-linux-64: 2.0.6 + turbo-linux-arm64: 2.0.6 + turbo-windows-64: 2.0.6 + turbo-windows-arm64: 2.0.6 dev: true /type-check@0.4.0: @@ -20584,20 +20084,6 @@ packages: dependencies: xml-name-validator: 5.0.0 - /wait-on@7.2.0(debug@3.2.7): - resolution: {integrity: sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ==} - engines: {node: '>=12.0.0'} - hasBin: true - dependencies: - axios: 1.6.8(debug@3.2.7) - joi: 17.12.3 - lodash: 4.17.21 - minimist: 1.2.8 - rxjs: 7.8.1 - transitivePeerDependencies: - - debug - dev: true - /walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} dependencies: @@ -20846,19 +20332,6 @@ packages: write-json-file: 3.2.0 dev: true - /ws@7.5.9: - resolution: {integrity: sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==} - engines: {node: '>=8.3.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - dev: true - /ws@8.16.0: resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==} engines: {node: '>=10.0.0'}