Skip to content
Open
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
2 changes: 2 additions & 0 deletions packages/slate/src/interfaces/transforms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { NodeTransforms } from './node'
import { SelectionTransforms } from './selection'
import { TextTransforms } from './text'

export { InsertFragmentFilter, InsertFragmentFilterOptions } from './text'

export const Transforms: GeneralTransforms &
NodeTransforms &
SelectionTransforms &
Expand Down
28 changes: 27 additions & 1 deletion packages/slate/src/interfaces/transforms/text.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { Editor, Location, Node, Path, Range, Transforms } from '../../index'
import {
Editor,
Element,
Location,
Node,
NodeEntry,
Path,
Range,
Transforms,
} from '../../index'
import { TextUnit } from '../../types/types'
import { getDefaultInsertLocation } from '../../utils'

Expand All @@ -11,11 +20,28 @@ export interface TextDeleteOptions {
voids?: boolean
}

export interface InsertFragmentFilterOptions {
/**
* The block entry that the fragment will be inserted into.
*/
blockEntry: NodeEntry<Element>
}

export type InsertFragmentFilter = (
node: Node,
options: InsertFragmentFilterOptions
) => boolean

export interface TextInsertFragmentOptions {
at?: Location
hanging?: boolean
voids?: boolean
batchDirty?: boolean
/**
* A filter function that controls whether each individual node is eligible for insertion.
* Return `true` to allow the node, `false` to skip it.
*/
filter?: InsertFragmentFilter
}

export interface TextInsertTextOptions {
Expand Down
77 changes: 69 additions & 8 deletions packages/slate/src/transforms-text/insert-fragment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const insertFragment: TextTransforms['insertFragment'] = (
options = {}
) => {
Editor.withoutNormalizing(editor, () => {
const { hanging = false, voids = false } = options
const { hanging = false, voids = false, filter } = options
let { at = getDefaultInsertLocation(editor), batchDirty = true } = options

if (!fragment.length) {
Expand Down Expand Up @@ -74,6 +74,47 @@ export const insertFragment: TextTransforms['insertFragment'] = (
voids,
})!
const [, blockPath] = blockMatch

const filterOptions = { blockEntry: blockMatch }
const filterNode = (node: Node): Node | null => {
if (!filter) return node

if (Text.isText(node)) {
return filter(node, filterOptions) ? node : null
}

if (Element.isElement(node)) {
if (!filter(node, filterOptions)) {
return null
}

const filteredChildren = node.children
.map(filterNode)
.filter((child): child is Descendant => child !== null)

// If all children were filtered out, return null for non-void elements
if (filteredChildren.length === 0 && !editor.isVoid(node)) {
return null
}

return { ...node, children: filteredChildren }
}

return node
}

const filteredFragment = filter
? (fragment
.map(filterNode)
.filter((node): node is Descendant => node !== null) as Node[])
: fragment

if (!filteredFragment.length) {
return
}

// Use filteredFragment instead of fragment from here on
fragment = filteredFragment
const isBlockStart = Editor.isStart(editor, at, blockPath)
const isBlockEnd = Editor.isEnd(editor, at, blockPath)
const isBlockEmpty = isBlockStart && isBlockEnd
Expand Down Expand Up @@ -180,19 +221,26 @@ export const insertFragment: TextTransforms['insertFragment'] = (
const isInlineStart = Editor.isStart(editor, at, inlinePath)
const isInlineEnd = Editor.isEnd(editor, at, inlinePath)

// When inserting block-level nodes into the middle, we still anchor to the
// current block path; after split Slate keeps the right half at the same
// path, so inserting at that path puts middles between halves.
const insertingIntoBlockMiddle = !isBlockStart && !isBlockEnd
const middleRef = Editor.pathRef(
editor,
isBlockEnd && !ends.length ? Path.next(blockPath) : blockPath
)

const endRef = Editor.pathRef(
editor,
isInlineEnd ? Path.next(inlinePath) : inlinePath
)

// If the fragment contains inlines in multiple distinct blocks, split the
// destination block.
const splitBlock = ends.length > 0
// Split the destination block if:
// - the fragment has trailing inlines to merge (original behavior), or
// - we're inserting block-level nodes into the middle of a block so they
// can appear between the two halves of the destination block.
const shouldSplitForMiddleBlocks =
middles.length > 0 && insertingIntoBlockMiddle
const splitBlock = ends.length > 0 || shouldSplitForMiddleBlocks

Transforms.splitNodes(editor, {
at,
Expand Down Expand Up @@ -223,7 +271,9 @@ export const insertFragment: TextTransforms['insertFragment'] = (
batchDirty,
})

if (isBlockEmpty && !starts.length && middles.length && !ends.length) {
const shouldDeleteEmptyBlock =
isBlockEmpty && !starts.length && middles.length && !ends.length
if (shouldDeleteEmptyBlock) {
Transforms.delete(editor, { at: blockPath, voids })
}

Expand Down Expand Up @@ -255,8 +305,19 @@ export const insertFragment: TextTransforms['insertFragment'] = (
}

if (path) {
const end = Editor.end(editor, path)
Transforms.select(editor, end)
// If the previous sibling is void (e.g., an inserted image block),
// place the cursor at the start of the right-half block.
const nodeAtPath = Node.get(editor, path)
const isVoidAtPath =
Element.isElement(nodeAtPath) && editor.isVoid(nodeAtPath)
if (isVoidAtPath) {
const nextPath = Path.next(path)
const start = Editor.start(editor, nextPath)
Transforms.select(editor, start)
} else {
const end = Editor.end(editor, path)
Transforms.select(editor, end)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/** @jsx jsx */
import { Transforms } from 'slate'
import { jsx } from '../../..'

export const run = (editor, options = {}) => {
Transforms.insertFragment(
editor,
<fragment>
<block void />
</fragment>,
options
)
}
export const input = (
<editor>
<block>
wo
<cursor />
rd
</block>
</editor>
)
export const output = (
<editor>
<block>wo</block>
<block void>
<text />
</block>
<block>
<cursor />
rd
</block>
</editor>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/** @jsx jsx */
import { Transforms, Element, Editor } from 'slate'
import { jsx } from '../../..'

export const run = (editor, options = {}) => {
Transforms.insertFragment(
editor,
<fragment>
<block>hello</block>
<block void>
<text />
</block>
<block>world</block>
</fragment>,
{
...options,
filter: (node, _filterOptions) => {
// Filter out void blocks but allow text and non-void blocks
if (
Element.isElement(node) &&
Editor.isBlock(editor, node) &&
editor.isVoid(node)
) {
return false
}
return true
},
}
)
}
export const input = (
<editor>
<block>
wo
<cursor />
rd
</block>
</editor>
)
// The void block is filtered out, but text blocks are inserted
export const output = (
<editor>
<block>wohello</block>
<block>
world
<cursor />
rd
</block>
</editor>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/** @jsx jsx */
import { Transforms, Element, Editor } from 'slate'
import { jsx } from '../../..'

export const run = (editor, options = {}) => {
Transforms.insertFragment(
editor,
<fragment>
<block void>
<text />
</block>
</fragment>,
{
...options,
filter: (node, filterOptions) => {
// Filter out void blocks when the destination block is a 'lic' type
const [blockNode] = filterOptions.blockEntry
if (
blockNode.type === 'lic' &&
Element.isElement(node) &&
Editor.isBlock(editor, node) &&
editor.isVoid(node)
) {
return false
}
return true
},
}
)
}
export const input = (
<editor>
<block type="lic">
wo
<cursor />
rd
</block>
</editor>
)
// Since the void block is filtered out when destination is 'lic', nothing should be inserted
export const output = (
<editor>
<block type="lic">
wo
<cursor />
rd
</block>
</editor>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/** @jsx jsx */
import { Transforms, Element, Editor } from 'slate'
import { jsx } from '../../..'

export const run = (editor, options = {}) => {
Transforms.insertFragment(
editor,
<fragment>
<block void>
<text />
</block>
</fragment>,
{
...options,
filter: (node, _filterOptions) => {
// Filter out void blocks when inserting into a specific block type
if (
Element.isElement(node) &&
Editor.isBlock(editor, node) &&
editor.isVoid(node)
) {
return false
}
return true
},
}
)
}
export const input = (
<editor>
<block>
wo
<cursor />
rd
</block>
</editor>
)
// Since the void block is filtered out, nothing should be inserted
export const output = (
<editor>
<block>
wo
<cursor />
rd
</block>
</editor>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/** @jsx jsx */
import { Transforms } from 'slate'
import { jsx } from '../../..'

export const run = (editor, options = {}) => {
Transforms.insertFragment(
editor,
<fragment>
<block>hello</block>
</fragment>,
{
...options,
// No filter provided - should work as normal
}
)
}
export const input = (
<editor>
<block>
wo
<cursor />
rd
</block>
</editor>
)
// Without filter, insertion works normally
export const output = (
<editor>
<block>
wohello
<cursor />
rd
</block>
</editor>
)