diff --git a/.changeset/brave-peaches-bake.md b/.changeset/brave-peaches-bake.md new file mode 100644 index 000000000..25026f910 --- /dev/null +++ b/.changeset/brave-peaches-bake.md @@ -0,0 +1,32 @@ +--- +'@portabletext/editor': major +--- + +feat!: remove `PortableTextEditor` React component + +## Migration + +If you were using `PortableTextEditor` as a React component, switch to `EditorProvider`: + +```diff +- import {PortableTextEditor} from '@portabletext/editor' ++ import {EditorProvider} from '@portabletext/editor' + +- ++ + +- ++ +``` + +The `PortableTextEditorProps` type export has also been removed. diff --git a/.changeset/config.json b/.changeset/config.json index cdb41b19a..ff893ba45 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -8,7 +8,7 @@ "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", - "ignore": ["docs", "playground", "example-basic", "example-legacy"], + "ignore": ["docs", "playground", "example-basic"], "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { "updateInternalDependents": "always", "onlyUpdatePeerDependentsWhenOutOfRange": true diff --git a/examples/legacy/.gitignore b/examples/legacy/.gitignore deleted file mode 100644 index a547bf36d..000000000 --- a/examples/legacy/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/examples/legacy/README.md b/examples/legacy/README.md deleted file mode 100644 index e273b625a..000000000 --- a/examples/legacy/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# React + TypeScript + Vite - -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: - -- Configure the top-level `parserOptions` property like this: - -```js -export default tseslint.config({ - languageOptions: { - // other options... - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - }, -}) -``` - -- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` -- Optionally add `...tseslint.configs.stylisticTypeChecked` -- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: - -```js -// eslint.config.js -import react from 'eslint-plugin-react' - -export default tseslint.config({ - // Set the react version - settings: {react: {version: '18.3'}}, - plugins: { - // Add the react plugin - react, - }, - rules: { - // other rules... - // Enable its recommended rules - ...react.configs.recommended.rules, - ...react.configs['jsx-runtime'].rules, - }, -}) -``` diff --git a/examples/legacy/eslint.config.js b/examples/legacy/eslint.config.js deleted file mode 100644 index 62ee28334..000000000 --- a/examples/legacy/eslint.config.js +++ /dev/null @@ -1,28 +0,0 @@ -import js from '@eslint/js' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import globals from 'globals' -import tseslint from 'typescript-eslint' - -export default tseslint.config( - {ignores: ['dist']}, - reactHooks.configs.flat.recommended, - { - extends: [js.configs.recommended, ...tseslint.configs.recommended], - files: ['**/*.{ts,tsx}'], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - }, - plugins: { - 'react-refresh': reactRefresh, - }, - rules: { - 'react-hooks/exhaustive-deps': 'error', - 'react-refresh/only-export-components': [ - 'warn', - {allowConstantExport: true}, - ], - }, - }, -) diff --git a/examples/legacy/index.html b/examples/legacy/index.html deleted file mode 100644 index e4b78eae1..000000000 --- a/examples/legacy/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Vite + React + TS - - -
- - - diff --git a/examples/legacy/package.json b/examples/legacy/package.json deleted file mode 100644 index cfd5fdb49..000000000 --- a/examples/legacy/package.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "example-legacy", - "version": "0.0.0", - "private": true, - "type": "module", - "scripts": { - "build": "tsc -b && vite build", - "check:lint": "eslint .", - "check:types": "tsc", - "clean": "del .turbo && del dist && del node_modules", - "dev": "vite", - "preview": "vite preview" - }, - "dependencies": { - "@portabletext/editor": "workspace:*", - "@portabletext/patches": "workspace:*", - "@sanity/schema": "^4.0.1", - "@xstate/react": "^4.1.3", - "react": "^19.2.0", - "react-dom": "^19.2.0", - "rxjs": "^7.8.1", - "xstate": "^5.18.2" - }, - "devDependencies": { - "@eslint/js": "^9.34.0", - "@sanity/types": "^4.0.1", - "@types/react": "^19.2.2", - "@types/react-dom": "^19.2.2", - "@vitejs/plugin-react": "^4.3.3", - "babel-plugin-react-compiler": "^1.0.0", - "eslint": "^9.38.0", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.14", - "globals": "^15.11.0", - "typescript": "catalog:", - "typescript-eslint": "^8.46.1", - "vite": "^5.4.10" - } -} diff --git a/examples/legacy/public/vite.svg b/examples/legacy/public/vite.svg deleted file mode 100644 index e7b8dfb1b..000000000 --- a/examples/legacy/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples/legacy/src/App.tsx b/examples/legacy/src/App.tsx deleted file mode 100644 index d3a7be235..000000000 --- a/examples/legacy/src/App.tsx +++ /dev/null @@ -1,407 +0,0 @@ -import { - MutationChange, - Patch, - PortableTextBlock, - PortableTextChild, - PortableTextEditable, - PortableTextEditor, - RenderAnnotationFunction, - RenderBlockFunction, - RenderChildFunction, - RenderDecoratorFunction, - RenderStyleFunction, - usePortableTextEditor, - usePortableTextEditorSelection, -} from '@portabletext/editor' -import {applyAll} from '@portabletext/patches' -import {useActorRef, useSelector} from '@xstate/react' -import {useEffect, useMemo} from 'react' -import {Subject} from 'rxjs' -import {ActorRefFrom, assign, emit, sendParent, setup} from 'xstate' -import './editor.css' -import {schema} from './schema' - -const editorMachine = setup({ - types: { - events: {} as - | MutationChange - | { - type: 'patches' - patches: MutationChange['patches'] - snapshot: MutationChange['snapshot'] - }, - emitted: {} as { - type: 'patches' - patches: MutationChange['patches'] - snapshot: MutationChange['snapshot'] - }, - }, -}).createMachine({ - id: 'editor', - on: { - mutation: { - actions: sendParent(({event, self}) => ({ - ...event, - type: 'editor.mutation', - editorId: self.id, - })), - }, - patches: { - actions: emit(({event}) => event), - }, - }, -}) - -const playgroundMachine = setup({ - types: { - context: {} as { - editors: Array> - value: Array | undefined - }, - events: {} as { - type: 'editor.mutation' - editorId: ActorRefFrom['id'] - patches: Array - snapshot: Array | undefined - }, - }, - actors: { - 'editor machine': editorMachine, - }, -}).createMachine({ - id: 'playground', - context: ({spawn}) => ({ - editors: [spawn(editorMachine), spawn(editorMachine)], - value: undefined, - }), - on: { - 'editor.mutation': { - actions: [ - ({context, event}) => { - for (const editor of context.editors) { - editor.send({ - type: 'patches', - patches: event.patches.map((patch) => ({ - ...patch, - origin: event.editorId === editor.id ? 'local' : 'remote', - })), - snapshot: event.snapshot, - }) - } - }, - assign({ - value: ({context, event}) => { - return applyAll(context.value, event.patches) - }, - }), - ], - }, - }, -}) - -/** - * WARNING: This is just an example app to make sure that legacy APIs are still - * working. Don't use this as a reference for your own project. - */ -function App() { - const playground = useActorRef(playgroundMachine) - const editors = useSelector(playground, (s) => s.context.editors) - const value = useSelector(playground, (s) => s.context.value) - - return ( - <> - {editors.map((editor) => ( - - ))} -
-        {JSON.stringify(value, null, 2)}
-      
- - ) -} - -function Editor(props: { - editorRef: ActorRefFrom - value: Array | undefined -}) { - const patches$ = useMemo( - () => - new Subject<{ - patches: Patch[] - snapshot: PortableTextBlock[] | undefined - }>(), - [], - ) - useEffect(() => { - props.editorRef.on('patches', (event) => { - patches$.next({ - patches: event.patches, - snapshot: event.snapshot, - }) - }) - }, [props.editorRef, patches$]) - - return ( -
- { - if (change.type === 'mutation') { - props.editorRef.send(change) - } - }} - > - - { - /** - * In a real-world scenario you would want to trigger a dialog - * here so you can ask the user to input the URL for the link. - */ - if (PortableTextEditor.isAnnotationActive(editor, 'link')) { - PortableTextEditor.removeAnnotation(editor, {name: 'link'}) - } else { - PortableTextEditor.addAnnotation( - editor, - {name: 'link'}, - {href: 'https://example.com'}, - ) - } - }, - }, - }} - style={{border: '1px solid black', padding: '0.5em'}} - renderDecorator={renderDecorator} - renderAnnotation={renderAnnotation} - renderBlock={renderBlock} - renderStyle={renderStyle} - renderChild={renderChild} - renderListItem={(props) => <>{props.children}} - /> - -
- ) -} - -const renderDecorator: RenderDecoratorFunction = (props) => { - if (props.value === 'strong') { - return {props.children} - } - if (props.value === 'em') { - return {props.children} - } - if (props.value === 'underline') { - return {props.children} - } - return <>{props.children} -} - -const renderAnnotation: RenderAnnotationFunction = (props) => { - if (props.schemaType.name === 'link') { - return {props.children} - } - - return <>{props.children} -} - -const renderBlock: RenderBlockFunction = (props) => { - if (props.schemaType.name === 'image' && isImage(props.value)) { - return ( -
- IMG: {props.value.src} -
- ) - } - - return
{props.children}
-} - -function isImage( - props: PortableTextBlock, -): props is PortableTextBlock & {src: string} { - return 'src' in props -} - -const renderStyle: RenderStyleFunction = (props) => { - if (props.schemaType.value === 'h1') { - return

{props.children}

- } - if (props.schemaType.value === 'h2') { - return

{props.children}

- } - if (props.schemaType.value === 'h3') { - return

{props.children}

- } - if (props.schemaType.value === 'blockquote') { - return
{props.children}
- } - return <>{props.children} -} - -const renderChild: RenderChildFunction = (props) => { - if (props.schemaType.name === 'stock-ticker' && isStockTicker(props.value)) { - return ( - - {props.value.symbol} - - ) - } - - return <>{props.children} -} - -function isStockTicker( - props: PortableTextChild, -): props is PortableTextChild & {symbol: string} { - return 'symbol' in props -} - -function Toolbar() { - const editor = usePortableTextEditor() - usePortableTextEditorSelection() - - const decoratorButtons = editor.schemaTypes.decorators.map((decorator) => ( - - )) - - const linkButton = ( - - ) - - const styleButtons = editor.schemaTypes.styles.map((style) => ( - - )) - - const listButtons = editor.schemaTypes.lists.map((list) => ( - - )) - - const imageButton = ( - - ) - - const stockTickerButton = ( - - ) - - return ( - <> -
{decoratorButtons}
-
{linkButton}
-
{styleButtons}
-
{listButtons}
-
{imageButton}
-
{stockTickerButton}
- - ) -} - -function DecoratorButton(props: {decorator: string}) { - const editor = usePortableTextEditor() - usePortableTextEditorSelection() - - return ( - - ) -} - -export default App diff --git a/examples/legacy/src/editor.css b/examples/legacy/src/editor.css deleted file mode 100644 index 62d721236..000000000 --- a/examples/legacy/src/editor.css +++ /dev/null @@ -1,187 +0,0 @@ -[role='textbox'] { - --list-padding: 1em; - /** - * First, we initialize a counter for each block level. - */ - counter-reset: level-1 level-2 level-3 level-4 level-5 level-6 level-7 level-8 - level-9 level-10; -} - -[data-list-item] { - display: flex; - gap: 0.5rem; -} -[data-list-item]::before { - text-align: right; - width: 1.5rem; -} - -[data-list-item='number'] { - align-items: baseline; -} -[data-list-item='number']::before { - font-size: 0.9rem; - font-variant-numeric: tabular-nums; -} - -[data-list-item='bullet'] { - align-items: center; -} -[data-list-item='bullet']::before { - font-size: 0.4rem; -} - -/** - * Then, the counter for each level is manually set to 1 whenever a list item - * with index 1 is encountered. - */ -[data-level='1'][data-list-index='1'] { - counter-set: level-1 1; -} -[data-level='2'][data-list-index='1'] { - counter-set: level-2 1; -} -[data-level='3'][data-list-index='1'] { - counter-set: level-3 1; -} -[data-level='4'][data-list-index='1'] { - counter-set: level-4 1; -} -[data-level='5'][data-list-index='1'] { - counter-set: level-5 1; -} -[data-level='6'][data-list-index='1'] { - counter-set: level-6 1; -} -[data-level='7'][data-list-index='1'] { - counter-set: level-7 1; -} -[data-level='8'][data-list-index='1'] { - counter-set: level-8 1; -} -[data-level='9'][data-list-index='1'] { - counter-set: level-9 1; -} -[data-level='10'][data-list-index='1'] { - counter-set: level-10 1; -} - -/** - * Thereafter, the count for each list level is incremented for each index - * greater than 1. - */ -[data-level='1']:not([data-list-index='1']) { - counter-increment: level-1; -} -[data-level='2']:not([data-list-index='1']) { - counter-increment: level-2; -} -[data-level='3']:not([data-list-index='1']) { - counter-increment: level-3; -} -[data-level='4']:not([data-list-index='1']) { - counter-increment: level-4; -} -[data-level='5']:not([data-list-index='1']) { - counter-increment: level-5; -} -[data-level='6']:not([data-list-index='1']) { - counter-increment: level-6; -} -[data-level='7']:not([data-list-index='1']) { - counter-increment: level-7; -} -[data-level='8']:not([data-list-index='1']) { - counter-increment: level-8; -} -[data-level='9']:not([data-list-index='1']) { - counter-increment: level-9; -} -[data-level='10']:not([data-list-index='1']) { - counter-increment: level-10; -} - -/** - * Finally, the calculated count is displayed in the list item. - */ -[data-list-item='number'][data-level='1']::before { - content: counter(level-1, decimal) '.'; -} -[data-list-item='number'][data-level='2']::before { - content: counter(level-2, lower-alpha) '.'; -} -[data-list-item='number'][data-level='3']::before { - content: counter(level-3, lower-roman) '.'; -} -[data-list-item='number'][data-level='4']::before { - content: counter(level-4, decimal) '.'; -} -[data-list-item='number'][data-level='5']::before { - content: counter(level-5, lower-alpha) '.'; -} -[data-list-item='number'][data-level='6']::before { - content: counter(level-6, lower-roman) '.'; -} -[data-list-item='number'][data-level='7']::before { - content: counter(level-7, decimal) '.'; -} -[data-list-item='number'][data-level='8']::before { - content: counter(level-8, lower-alpha) '.'; -} -[data-list-item='number'][data-level='9']::before { - content: counter(level-9, lower-roman) '.'; -} -[data-list-item='number'][data-level='10']::before { - content: counter(level-10, decimal) '.'; -} - -/** - * Visual display of bulleted list items - */ -[data-list-item='bullet'][data-level='1']::before, -[data-list-item='bullet'][data-level='4']::before, -[data-list-item='bullet'][data-level='7']::before, -[data-list-item='bullet'][data-level='10']::before { - content: '●'; -} -[data-list-item='bullet'][data-level='2']::before, -[data-list-item='bullet'][data-level='5']::before, -[data-list-item='bullet'][data-level='8']::before { - content: '○'; -} -[data-list-item='bullet'][data-level='3']::before, -[data-list-item='bullet'][data-level='6']::before, -[data-list-item='bullet'][data-level='9']::before { - content: '■'; -} - -/** - * Padding for each level of list item - */ -[data-level='2'] { - padding-left: calc(var(--list-padding)); -} -[data-level='3'] { - padding-left: calc(var(--list-padding) * 2); -} -[data-level='4'] { - padding-left: calc(var(--list-padding) * 3); -} -[data-level='5'] { - padding-left: calc(var(--list-padding) * 4); -} -[data-level='6'] { - padding-left: calc(var(--list-padding) * 5); -} -[data-level='7'] { - padding-left: calc(var(--list-padding) * 6); -} -[data-level='8'] { - padding-left: calc(var(--list-padding) * 7); -} -[data-level='9'] { - padding-left: calc(var(--list-padding) * 8); -} -[data-level='10'] { - padding-left: calc(var(--list-padding) * 9); -} diff --git a/examples/legacy/src/main.tsx b/examples/legacy/src/main.tsx deleted file mode 100644 index 551f4cbe5..000000000 --- a/examples/legacy/src/main.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import {StrictMode} from 'react' -import {createRoot} from 'react-dom/client' -import App from './App.tsx' - -createRoot(document.getElementById('root')!).render( - - - , -) diff --git a/examples/legacy/src/schema.ts b/examples/legacy/src/schema.ts deleted file mode 100644 index fed114a90..000000000 --- a/examples/legacy/src/schema.ts +++ /dev/null @@ -1,66 +0,0 @@ -import {Schema} from '@sanity/schema' -import {defineField, defineType} from '@sanity/types' - -const imageType = defineType({ - name: 'custom image', - title: 'Image', - type: 'object', - fields: [ - defineField({ - name: 'url', - type: 'string', - }), - ], -}) - -const stockTickerType = defineType({ - name: 'stock-ticker', - type: 'object', - fields: [ - defineField({ - name: 'symbol', - type: 'string', - }), - ], -}) - -const portableTextType = defineType({ - type: 'array', - name: 'body', - of: [ - { - 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'}, - ], - marks: { - annotations: [ - { - name: 'comment', - type: 'object', - fields: [{type: 'string', name: 'text'}], - }, - { - name: 'link', - type: 'object', - fields: [{type: 'string', name: 'href'}], - }, - ], - }, - of: [{type: 'stock-ticker'}], - }, - {type: 'custom image'}, - ], -}) - -export const schema = Schema.compile({ - types: [portableTextType, imageType, stockTickerType], -}).get('body') diff --git a/examples/legacy/src/vite-env.d.ts b/examples/legacy/src/vite-env.d.ts deleted file mode 100644 index 11f02fe2a..000000000 --- a/examples/legacy/src/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/examples/legacy/tsconfig.app.json b/examples/legacy/tsconfig.app.json deleted file mode 100644 index f867de0dd..000000000 --- a/examples/legacy/tsconfig.app.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "Bundler", - "allowImportingTsExtensions": true, - "isolatedModules": true, - "moduleDetection": "force", - "noEmit": true, - "jsx": "react-jsx", - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true - }, - "include": ["src"] -} diff --git a/examples/legacy/tsconfig.json b/examples/legacy/tsconfig.json deleted file mode 100644 index b3de6fde9..000000000 --- a/examples/legacy/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "files": [], - "references": [ - {"path": "./tsconfig.app.json"}, - {"path": "./tsconfig.node.json"} - ] -} diff --git a/examples/legacy/tsconfig.node.json b/examples/legacy/tsconfig.node.json deleted file mode 100644 index abcd7f0da..000000000 --- a/examples/legacy/tsconfig.node.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", - "target": "ES2022", - "lib": ["ES2023"], - "module": "ESNext", - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "Bundler", - "allowImportingTsExtensions": true, - "isolatedModules": true, - "moduleDetection": "force", - "noEmit": true, - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true - }, - "include": ["vite.config.ts"] -} diff --git a/examples/legacy/vite.config.ts b/examples/legacy/vite.config.ts deleted file mode 100644 index 87764ac2f..000000000 --- a/examples/legacy/vite.config.ts +++ /dev/null @@ -1,28 +0,0 @@ -import path from 'node:path' -import react from '@vitejs/plugin-react' -import {defineConfig} from 'vite' - -// https://vite.dev/config/ -export default defineConfig({ - plugins: [ - react({ - babel: {plugins: [['babel-plugin-react-compiler', {target: '19'}]]}, - }), - ], - resolve: { - alias: { - '@portabletext/editor': path.resolve( - __dirname, - '../../packages/editor/src', - ), - '@portabletext/block-tools': path.resolve( - __dirname, - '../../packages/block-tools/src', - ), - '@portabletext/patches': path.resolve( - __dirname, - '../../packages/patches/src', - ), - }, - }, -}) diff --git a/package.json b/package.json index 4bce69f68..f0cce63f3 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,6 @@ "build:docs": "turbo build --filter=docs", "build:editor": "turbo build --filter=@portabletext/editor...", "build:example-basic": "turbo build --filter=example-basic...", - "build:example-legacy": "turbo build --filter=example-legacy...", "build:keyboard-shortcuts": "turbo build --filter=@portabletext/keyboard-shortcuts...", "build:markdown": "turbo build --filter=@portabletext/markdown...", "build:patches": "turbo build --filter=@portabletext/patches...", @@ -29,7 +28,6 @@ "dev:docs": "turbo dev --filter=docs", "dev:editor": "turbo dev --filter=@portabletext/editor...", "dev:example-basic": "turbo dev --filter=example-basic", - "dev:example-legacy": "turbo dev --filter=example-legacy", "dev:keyboard-shortcuts": "turbo dev --filter=@portabletext/keyboard-shortcuts...", "dev:playground": "turbo dev --filter=playground", "dev:plugin-emoji-picker": "turbo dev --filter=@portabletext/plugin-emoji-picker...", diff --git a/packages/editor/src/editor/PortableTextEditor.tsx b/packages/editor/src/editor/PortableTextEditor.tsx index 06aa04a45..b256c1cab 100644 --- a/packages/editor/src/editor/PortableTextEditor.tsx +++ b/packages/editor/src/editor/PortableTextEditor.tsx @@ -3,107 +3,36 @@ import type { PortableTextChild, PortableTextObject, } from '@portabletext/schema' -import { - Component, - useEffect, - type MutableRefObject, - type PropsWithChildren, -} from 'react' import {Subject} from 'rxjs' -import {Slate} from 'slate-react' -import {stopActor} from '../internal-utils/stop-actor' import type { AddedAnnotationPaths, EditableAPI, EditableAPIDeleteOptions, - EditorChange, EditorChanges, EditorSelection, - PatchObservable, PortableTextMemberSchemaTypes, } from '../types/editor' import type {Path} from '../types/paths' -import type {ArrayDefinition, ArraySchemaType} from '../types/sanity-types' -import {createInternalEditor, type InternalEditor} from './create-editor' -import {EditorActorContext} from './editor-actor-context' -import type {EditorActor} from './editor-machine' -import {eventToChange} from './event-to-change' -import type {MutationActor} from './mutation-machine' -import {RelayActorContext} from './relay-actor-context' -import type {RelayActor} from './relay-machine' -import type {SyncActor} from './sync-machine' -import {PortableTextEditorContext} from './usePortableTextEditor' +import type {InternalEditor} from './create-editor' /** - * Props for the PortableTextEditor component - * - * @public - * @deprecated Use `EditorProvider` instead - */ -export type PortableTextEditorProps< - TEditor extends InternalEditor | undefined = undefined, -> = PropsWithChildren< - TEditor extends InternalEditor - ? { - /** - * @internal - */ - editor: TEditor - } - : { - editor?: undefined - - /** - * Function that gets called when the editor changes the value - */ - onChange: (change: EditorChange) => void - - /** - * Schema type for the portable text field - */ - schemaType: ArraySchemaType | ArrayDefinition - - /** - * 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 - - /** - * Whether or not the editor should be in read-only mode - */ - readOnly?: boolean - - /** - * The current value of the portable text field - */ - value?: PortableTextBlock[] - - /** - * A ref to the editor instance - */ - editorRef?: MutableRefObject - } -> - -/** - * The main Portable Text Editor component. * @public - * @deprecated Use `EditorProvider` instead + * @deprecated Use `useEditor()` instead + * + * ``` + * import {useEditor} from '@portabletext/editor' + * + * // Get the editor instance + * const editor = useEditor() + * + * // Send events to the editor + * editor.send(...) + * + * // Derive state from the editor + * const state = useEditorSelector(editor, snapshot => ...) + * ``` */ -export class PortableTextEditor extends Component< - PortableTextEditorProps -> { - public static displayName = 'PortableTextEditor' +export class PortableTextEditor { /** * An observable of all the editor changes. */ @@ -121,120 +50,12 @@ export class PortableTextEditor extends Component< */ private editable: EditableAPI - private actors?: { - editorActor: EditorActor - mutationActor: MutationActor - relayActor: RelayActor - syncActor: SyncActor - } - - private subscriptions: Array<() => () => void> = [] - private unsubscribers: Array<() => void> = [] - - constructor(props: PortableTextEditorProps) { - super(props) - - if (props.editor) { - this.editor = props.editor as InternalEditor - this.schemaTypes = this.editor._internal.editorActor - .getSnapshot() - .context.getLegacySchema() - } else { - const {actors, editor, subscriptions} = createInternalEditor({ - initialValue: props.value, - keyGenerator: props.keyGenerator, - readOnly: props.readOnly, - schema: props.schemaType, - }) - - this.subscriptions = subscriptions - this.actors = actors - - this.editor = editor - this.schemaTypes = actors.editorActor - .getSnapshot() - .context.getLegacySchema() - } - - this.editable = this.editor._internal.editable - } - - override componentDidMount(): void { - if (!this.actors) { - return - } - - for (const subscription of this.subscriptions) { - this.unsubscribers.push(subscription()) - } - - const relayActorSubscription = this.actors.relayActor.on('*', (event) => { - const change = eventToChange(event) - - if (!change) { - return - } - - if (!this.props.editor) { - this.props.onChange(change) - } - - this.change$.next(change) - }) - - this.unsubscribers.push(relayActorSubscription.unsubscribe) - - this.actors.editorActor.start() - this.actors.mutationActor.start() - this.actors.relayActor.start() - this.actors.syncActor.start() - } - - override componentDidUpdate(prevProps: PortableTextEditorProps) { - // Set up the schema type lookup table again if the source schema type changes - if ( - !this.props.editor && - !prevProps.editor && - this.props.schemaType !== prevProps.schemaType - ) { - console.warn('Updating schema type is no longer supported') - } - - if (!this.props.editor && !prevProps.editor) { - if (this.props.readOnly !== prevProps.readOnly) { - this.editor._internal.editorActor.send({ - type: 'update readOnly', - readOnly: this.props.readOnly ?? false, - }) - } - - if (this.props.value !== prevProps.value) { - this.editor.send({ - type: 'update value', - value: this.props.value, - }) - } - - if ( - this.props.editorRef !== prevProps.editorRef && - this.props.editorRef - ) { - this.props.editorRef.current = this - } - } - } - - override componentWillUnmount(): void { - for (const unsubscribe of this.unsubscribers) { - unsubscribe() - } - - if (this.actors) { - stopActor(this.actors.editorActor) - stopActor(this.actors.mutationActor) - stopActor(this.actors.relayActor) - stopActor(this.actors.syncActor) - } + constructor(config: {editor: InternalEditor}) { + this.editor = config.editor + this.schemaTypes = config.editor._internal.editorActor + .getSnapshot() + .context.getLegacySchema() + this.editable = config.editor._internal.editable } public setEditable = (editable: EditableAPI) => { @@ -244,35 +65,6 @@ export class PortableTextEditor extends Component< } } - override render() { - const legacyPatches = !this.props.editor - ? (this.props.incomingPatches$ ?? this.props.patches$) - : undefined - - return ( - <> - {legacyPatches ? ( - - ) : null} - - - - - {this.props.children} - - - - - - ) - } - /** * @deprecated * Use built-in selectors or write your own: https://www.portabletext.org/reference/selectors/ @@ -757,23 +549,3 @@ export class PortableTextEditor extends Component< return editor.editable?.isSelectionsOverlapping(selectionA, selectionB) } } - -function RoutePatchesObservableToEditorActor(props: { - editorActor: EditorActor - patches$: PatchObservable -}) { - useEffect(() => { - const subscription = props.patches$.subscribe((payload) => { - props.editorActor.send({ - type: 'patches', - ...payload, - }) - }) - - return () => { - subscription.unsubscribe() - } - }, [props.editorActor, props.patches$]) - - return null -} diff --git a/packages/editor/src/editor/editor-provider.tsx b/packages/editor/src/editor/editor-provider.tsx index c9cbe7ce4..a63005514 100644 --- a/packages/editor/src/editor/editor-provider.tsx +++ b/packages/editor/src/editor/editor-provider.tsx @@ -7,10 +7,7 @@ import {createInternalEditor} from './create-editor' import {EditorActorContext} from './editor-actor-context' import {EditorContext} from './editor-context' import {eventToChange} from './event-to-change' -import { - PortableTextEditor, - type PortableTextEditorProps, -} from './PortableTextEditor' +import {PortableTextEditor} from './PortableTextEditor' import {RelayActorContext} from './relay-actor-context' import {PortableTextEditorContext} from './usePortableTextEditor' @@ -45,7 +42,7 @@ export function EditorProvider(props: EditorProviderProps) { const internalEditor = createInternalEditor(props.initialConfig) const portableTextEditor = new PortableTextEditor({ editor: internalEditor.editor, - } as unknown as PortableTextEditorProps) + }) return {internalEditor, portableTextEditor} }) diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index 3115e6573..0b3b440e4 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -41,7 +41,6 @@ export {usePortableTextEditor} from './editor/usePortableTextEditor' export {usePortableTextEditorSelection} from './editor/usePortableTextEditorSelection' export {defaultKeyGenerator as keyGenerator} from './utils/key-generator' export {PortableTextEditor} from './editor/PortableTextEditor' -export type {PortableTextEditorProps} from './editor/PortableTextEditor' export type {EditorEmittedEvent, MutationEvent} from './editor/relay-machine' export {useEditor} from './editor/use-editor' export type {AddedAnnotationPaths} from './types/editor'