Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/vendor-slate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@portabletext/editor': minor
---

feat: vendor Slate into the editor

Vendors `slate`, `slate-dom`, and `slate-react` into the package to remove external dependencies and enable direct modifications to the Slate layer. React Compiler exclusions added for the vendored code. No public API changes.
10 changes: 9 additions & 1 deletion packages/editor/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@ import {globalIgnores} from 'eslint/config'
import tseslint from 'typescript-eslint'

export default tseslint.config([
globalIgnores(['coverage', 'dist', 'lib', '**/__tests__/**']),
globalIgnores([
'coverage',
'dist',
'lib',
'**/__tests__/**',
'src/slate/**',
'src/slate-dom/**',
'src/slate-react/**',
]),
reactHooks.configs.flat.recommended,
{
files: ['src/**/*.{cjs,mjs,js,jsx,ts,tsx}'],
Expand Down
8 changes: 7 additions & 1 deletion packages/editor/package.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ export default defineConfig({
noImplicitSideEffects: 'error',
},
babel: {reactCompiler: true},
reactCompilerOptions: {target: '19'},
reactCompilerOptions: {
target: '19',
sources: (filename: string) =>
!filename.includes('/src/slate/') &&
!filename.includes('/src/slate-dom/') &&
!filename.includes('/src/slate-react/'),
},
dts: 'rolldown',
})
7 changes: 4 additions & 3 deletions packages/editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"test:watch": "vitest"
},
"dependencies": {
"@juggle/resize-observer": "^3.4.0",
"@portabletext/block-tools": "workspace:^",
"@portabletext/keyboard-shortcuts": "workspace:^",
"@portabletext/markdown": "workspace:^",
Expand All @@ -94,9 +95,8 @@
"@sanity/types": "^5.9.0",
"@xstate/react": "^6.0.0",
"debug": "^4.4.3",
"slate": "^0.120.0",
"slate-dom": "^0.119.0",
"slate-react": "^0.120.0",
"is-hotkey": "^0.2.0",
"scroll-into-view-if-needed": "^3.1.0",
"xstate": "^5.25.0"
},
"devDependencies": {
Expand All @@ -106,6 +106,7 @@
"@sanity/pkg-utils": "^10.2.1",
"@sanity/tsconfig": "^2.1.0",
"@types/debug": "^4.1.12",
"@types/is-hotkey": "^0.1.10",
"@types/node": "^20",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
Expand Down
27 changes: 27 additions & 0 deletions packages/editor/src/behaviors/behavior.abstract.keyboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {createKeyboardShortcut} from '@portabletext/keyboard-shortcuts'
import {isTextBlock} from '@portabletext/schema'
import {defaultKeyboardShortcuts} from '../editor/default-keyboard-shortcuts'
import {getFocusBlock} from '../selectors/selector.get-focus-block'
import {getFocusBlockObject} from '../selectors/selector.get-focus-block-object'
import {getFocusInlineObject} from '../selectors/selector.get-focus-inline-object'
import {getPreviousBlock} from '../selectors/selector.get-previous-block'
import {isSelectionCollapsed} from '../selectors/selector.is-selection-collapsed'
Expand Down Expand Up @@ -52,6 +53,32 @@ export const abstractKeyboardBehaviors = [
actions: [() => [raise({type: 'delete.forward', unit: 'character'})]],
}),

/**
* When Backspace is pressed on a block object, raise a `delete.backward`
* event. With childless voids, some browsers (WebKit) don't fire
* `beforeinput` on contentEditable=false elements, so the keydown
* handler must initiate the delete.
*/
defineBehavior({
on: 'keyboard.keydown',
guard: ({snapshot, event}) =>
defaultKeyboardShortcuts.backspace.guard(event.originEvent) &&
getFocusBlockObject(snapshot),
actions: [() => [raise({type: 'delete.backward', unit: 'character'})]],
}),

/**
* When Delete is pressed on a block object, raise a `delete.forward`
* event. Same rationale as above.
*/
defineBehavior({
on: 'keyboard.keydown',
guard: ({snapshot, event}) =>
defaultKeyboardShortcuts.delete.guard(event.originEvent) &&
getFocusBlockObject(snapshot),
actions: [() => [raise({type: 'delete.forward', unit: 'character'})]],
}),

defineBehavior({
on: 'keyboard.keydown',
guard: ({event}) =>
Expand Down
14 changes: 7 additions & 7 deletions packages/editor/src/editor/Editable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,19 @@ import {
type KeyboardEvent,
type TextareaHTMLAttributes,
} from 'react'
import {Editor, Transforms, type Text} from 'slate'
import {debug} from '../internal-utils/debug'
import {getEventPosition} from '../internal-utils/event-position'
import {normalizeSelection} from '../internal-utils/selection'
import {slateRangeToSelection} from '../internal-utils/slate-utils'
import {toSlateRange} from '../internal-utils/to-slate-range'
import {Editor, Transforms, type Text} from '../slate'
import {
ReactEditor,
Editable as SlateEditable,
useSlate,
type RenderElementProps,
type RenderLeafProps,
} from 'slate-react'
import {debug} from '../internal-utils/debug'
import {getEventPosition} from '../internal-utils/event-position'
import {normalizeSelection} from '../internal-utils/selection'
import {slateRangeToSelection} from '../internal-utils/slate-utils'
import {toSlateRange} from '../internal-utils/to-slate-range'
} from '../slate-react'
import type {
EditorSelection,
OnCopyFn,
Expand Down
4 changes: 2 additions & 2 deletions packages/editor/src/editor/create-editable-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import {
type PortableTextChild,
type PortableTextObject,
} from '@portabletext/schema'
import {Editor, Range, Text, Transforms} from 'slate'
import {ReactEditor} from 'slate-react'
import {
isListItemActive,
isStyleActive,
Expand All @@ -18,6 +16,8 @@ import {getFocusBlock} from '../selectors/selector.get-focus-block'
import {getFocusSpan} from '../selectors/selector.get-focus-span'
import {getSelectedValue} from '../selectors/selector.get-selected-value'
import {isActiveAnnotation} from '../selectors/selector.is-active-annotation'
import {Editor, Range, Text, Transforms} from '../slate'
import {ReactEditor} from '../slate-react'
import type {
EditableAPI,
EditableAPIDeleteOptions,
Expand Down
14 changes: 8 additions & 6 deletions packages/editor/src/editor/create-slate-editor.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {createEditor, type Descendant} from 'slate'
import {withReact} from 'slate-react'
import {buildIndexMaps} from '../internal-utils/build-index-maps'
import {createPlaceholderBlock} from '../internal-utils/create-placeholder-block'
import {debug} from '../internal-utils/debug'
import {createEditor, type Descendant} from '../slate'
import {plugins} from '../slate-plugins/slate-plugins'
import {withReact} from '../slate-react'
import {setSpanTypeName} from '../slate/span-type-config'
import type {PortableTextSlateEditor} from '../types/slate-editor'
import type {EditorActor} from './editor-machine'
import type {RelayActor} from './relay-machine'
Expand All @@ -22,9 +23,10 @@ export type SlateEditor = {
export function createSlateEditor(config: SlateEditorConfig): SlateEditor {
debug.setup('creating new slate editor instance')

const placeholderBlock = createPlaceholderBlock(
config.editorActor.getSnapshot().context,
)
const context = config.editorActor.getSnapshot().context
setSpanTypeName(context.schema.span.name)

const placeholderBlock = createPlaceholderBlock(context)

const editor = createEditor()

Expand Down Expand Up @@ -56,7 +58,7 @@ export function createSlateEditor(config: SlateEditorConfig): SlateEditor {

buildIndexMaps(
{
schema: config.editorActor.getSnapshot().context.schema,
schema: context.schema,
value: instance.value,
},
{
Expand Down
4 changes: 2 additions & 2 deletions packages/editor/src/editor/editor-dom.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {Editor} from 'slate'
import {DOMEditor} from 'slate-dom'
import type {BehaviorEvent} from '../behaviors/behavior.types.event'
import {toSlateRange} from '../internal-utils/to-slate-range'
import {getSelectionEndBlock, getSelectionStartBlock} from '../selectors'
import {Editor} from '../slate'
import {DOMEditor} from '../slate-dom'
import type {PickFromUnion} from '../type-utils'
import type {PortableTextSlateEditor} from '../types/slate-editor'
import type {EditorSnapshot} from './editor-snapshot'
Expand Down
6 changes: 3 additions & 3 deletions packages/editor/src/editor/editor-machine.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import type {Patch} from '@portabletext/patches'
import type {PortableTextBlock} from '@portabletext/schema'
import {Transforms} from 'slate'
import {EDITOR_TO_PENDING_SELECTION} from 'slate-dom'
import {ReactEditor} from 'slate-react'
import {
assertEvent,
assign,
Expand All @@ -22,6 +19,9 @@ import type {Converter} from '../converters/converter.types'
import {debug} from '../internal-utils/debug'
import type {EventPosition} from '../internal-utils/event-position'
import {sortByPriority} from '../priority/priority.sort'
import {Transforms} from '../slate'
import {EDITOR_TO_PENDING_SELECTION} from '../slate-dom'
import {ReactEditor} from '../slate-react'
import type {NamespaceEvent, OmitFromUnion} from '../type-utils'
import type {
EditorSelection,
Expand Down
2 changes: 1 addition & 1 deletion packages/editor/src/editor/editor-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type React from 'react'
import {useEffect, useState} from 'react'
import {Slate} from 'slate-react'
import type {EditorConfig} from '../editor'
import {stopActor} from '../internal-utils/stop-actor'
import {Slate} from '../slate-react'
import {createInternalEditor} from './create-editor'
import {EditorActorContext} from './editor-actor-context'
import {EditorContext} from './editor-context'
Expand Down
2 changes: 1 addition & 1 deletion packages/editor/src/editor/mutation-machine.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type {Patch} from '@portabletext/patches'
import type {PortableTextBlock} from '@portabletext/schema'
import {Editor} from 'slate'
import type {ActorRefFrom} from 'xstate'
import {
and,
Expand All @@ -15,6 +14,7 @@ import {
type AnyEventObject,
} from 'xstate'
import {debug} from '../internal-utils/debug'
import {Editor} from '../slate'
import type {PortableTextSlateEditor} from '../types/slate-editor'
import type {EditorSchema} from './editor-schema'
import type {PatchEvent} from './relay-machine'
Expand Down
2 changes: 1 addition & 1 deletion packages/editor/src/editor/perform-hotkey.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type {KeyboardEvent} from 'react'
import type {Editor} from 'slate'
import {isHotkey} from '../internal-utils/is-hotkey'
import type {Editor} from '../slate'
import type {HotkeyOptions} from '../types/options'
import type {EditorActor} from './editor-machine'
import type {PortableTextEditor} from './PortableTextEditor'
Expand Down
26 changes: 16 additions & 10 deletions packages/editor/src/editor/range-decorations-machine.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
import {
Element,
Path,
Range,
type BaseRange,
type NodeEntry,
type Operation,
} from 'slate'
import {
and,
assign,
Expand All @@ -18,6 +10,14 @@ import {isDeepEqual} from '../internal-utils/equality'
import {moveRangeByOperation} from '../internal-utils/move-range-by-operation'
import {slateRangeToSelection} from '../internal-utils/slate-utils'
import {toSlateRange} from '../internal-utils/to-slate-range'
import {
Element,
Path,
Range,
type BaseRange,
type NodeEntry,
type Operation,
} from '../slate'
import type {RangeDecoration} from '../types/editor'
import type {PortableTextSlateEditor} from '../types/slate-editor'
import {isEmptyTextBlock} from '../utils'
Expand Down Expand Up @@ -393,7 +393,11 @@ function createDecorate(
return []
}

if (!Element.isElement(node) || node.children.length === 0) {
if (
!Element.isElement(node) ||
!node.children ||
node.children.length === 0
) {
return []
}

Expand All @@ -403,11 +407,13 @@ function createDecorate(
return []
}

const children = node.children

return slateEditor.decoratedRanges.filter((decoratedRange) => {
// Special case in order to only return one decoration for collapsed ranges
if (Range.isCollapsed(decoratedRange)) {
// Collapsed ranges should only be decorated if they are on a block child level (length 2)
return node.children.some(
return children.some(
(_, childIndex) =>
Path.equals(decoratedRange.anchor.path, [blockIndex, childIndex]) &&
Path.equals(decoratedRange.focus.path, [blockIndex, childIndex]),
Expand Down
4 changes: 2 additions & 2 deletions packages/editor/src/editor/render.block-object.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type {PortableTextObject} from '@portabletext/schema'
import {useContext, useRef, type ReactElement} from 'react'
import type {Element as SlateElement} from 'slate'
import type {RenderElementProps} from 'slate-react'
import type {DropPosition} from '../behaviors/behavior.core.drop-position'
import type {Element as SlateElement} from '../slate'
import type {RenderElementProps} from '../slate-react'
import type {
BlockRenderProps,
PortableTextMemberSchemaTypes,
Expand Down
9 changes: 5 additions & 4 deletions packages/editor/src/editor/render.element.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {isTextBlock} from '@portabletext/schema'
import {useSelector} from '@xstate/react'
import {useContext, type ReactElement} from 'react'
import type {Element as SlateElement} from 'slate'
import {useSlateStatic, type RenderElementProps} from 'slate-react'
import type {DropPosition} from '../behaviors/behavior.core.drop-position'
import type {Element as SlateElement} from '../slate'
import {useSlateStatic, type RenderElementProps} from '../slate-react'
import type {
RenderBlockFunction,
RenderChildFunction,
Expand Down Expand Up @@ -34,8 +34,9 @@ export function RenderElement(props: {
)
const slateStatic = useSlateStatic()

const isInline =
'__inline' in props.element && props.element.__inline === true
const isInline = schema.inlineObjects.some(
(obj) => obj.name === props.element._type,
)

if (isInline) {
return (
Expand Down
15 changes: 5 additions & 10 deletions packages/editor/src/editor/render.inline-object.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {useContext, useRef, type ReactElement} from 'react'
import type {Element as SlateElement} from 'slate'
import {DOMEditor} from 'slate-dom'
import {useSlateStatic, type RenderElementProps} from 'slate-react'
import {getPointBlock} from '../internal-utils/slate-utils'
import type {Element as SlateElement} from '../slate'
import {DOMEditor} from '../slate-dom'
import {useSlateStatic, type RenderElementProps} from '../slate-react'
import type {
BlockChildRenderProps,
PortableTextMemberSchemaTypes,
Expand Down Expand Up @@ -63,13 +63,8 @@ export function RenderInlineObject(props: {
? selectionState.focusedChildPath === serializedPath
: false

const inlineObject = {
_key: props.element._key,
_type: props.element._type,
...('value' in props.element && typeof props.element.value === 'object'
? props.element.value
: {}),
}
// Strip the void-child `children` array to get the PT inline object
const {children: _voidChildren, ...inlineObject} = props.element

return (
<span
Expand Down
4 changes: 2 additions & 2 deletions packages/editor/src/editor/render.leaf.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {useSelector} from '@xstate/react'
import {useContext, type CSSProperties} from 'react'
import type {Text} from 'slate'
import type {RenderLeafProps} from 'slate-react'
import type {Text} from '../slate'
import type {RenderLeafProps} from '../slate-react'
import type {
RangeDecoration,
RenderAnnotationFunction,
Expand Down
2 changes: 1 addition & 1 deletion packages/editor/src/editor/render.span.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {useSelector} from '@xstate/react'
import {useContext, useMemo, useRef, type ReactElement} from 'react'
import {useSlateStatic, type RenderLeafProps} from 'slate-react'
import {useSlateStatic, type RenderLeafProps} from '../slate-react'
import type {
BlockAnnotationRenderProps,
BlockChildRenderProps,
Expand Down
4 changes: 2 additions & 2 deletions packages/editor/src/editor/render.text-block.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type {PortableTextTextBlock} from '@portabletext/schema'
import {useContext, useRef, type ReactElement} from 'react'
import type {Element as SlateElement} from 'slate'
import {useSlateSelector, type RenderElementProps} from 'slate-react'
import type {DropPosition} from '../behaviors/behavior.core.drop-position'
import type {Element as SlateElement} from '../slate'
import {useSlateSelector, type RenderElementProps} from '../slate-react'
import type {
BlockListItemRenderProps,
BlockRenderProps,
Expand Down
Loading
Loading