Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -172,4 +172,95 @@ describe(applyOperationToPortableText.name, () => {
},
])
})

test('updating block object with a field named "text"', () => {
const keyGenerator = createTestKeyGenerator()
const k0 = keyGenerator()

expect(
applyOperationToPortableText(
createContext(),
[
{
_type: 'quote',
_key: k0,
text: 'h',
},
],
{
type: 'set_node',
path: [0],
properties: {
value: {text: 'h'},
},
newProperties: {
value: {text: 'hello'},
},
},
),
).toEqual([
{
_type: 'quote',
_key: k0,
text: 'hello',
},
])
})

test('updating inline object with a field named "text"', () => {
const keyGenerator = createTestKeyGenerator()
const k0 = keyGenerator()
const k1 = keyGenerator()
const k2 = keyGenerator()
const k3 = keyGenerator()

expect(
applyOperationToPortableText(
createContext(),
[
{
_key: k0,
_type: 'block',
children: [
{
_key: k1,
_type: 'span',
text: '',
},
{
_key: k2,
_type: 'mention',
text: 'J',
},
{
_key: k3,
_type: 'span',
text: '',
},
],
},
],
{
type: 'set_node',
path: [0, 1],
properties: {
value: {text: 'J'},
},
newProperties: {
value: {text: 'John Doe'},
},
},
),
).toEqual([
{
_type: 'block',
_key: k0,
children: [
{_type: 'span', _key: k1, text: ''},
{_type: 'mention', _key: k2, text: 'John Doe'},
{_type: 'span', _key: k3, text: ''},
],
},
])
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ function applyOperationToPortableTextDraft(
break
}

if (isPartialSpanNode(insertedNode)) {
if (isPartialSpanNode(context, insertedNode)) {
// Text nodes can be inserted as is

parent.children.splice(index, 0, insertedNode)
Expand Down Expand Up @@ -161,7 +161,10 @@ function applyOperationToPortableTextDraft(

const index = path[path.length - 1]

if (isPartialSpanNode(node) && isPartialSpanNode(prev)) {
if (
isPartialSpanNode(context, node) &&
isPartialSpanNode(context, prev)
) {
prev.text += node.text
} else if (
isTextBlockNode(context, node) &&
Expand Down Expand Up @@ -342,7 +345,7 @@ function applyOperationToPortableTextDraft(
break
}

if (isPartialSpanNode(node)) {
if (isPartialSpanNode(context, node)) {
for (const key in newProperties) {
if (key === 'text') {
break
Expand Down
132 changes: 132 additions & 0 deletions packages/editor/src/internal-utils/portable-text-node.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import {compileSchema, defineSchema} from '@portabletext/schema'
import {describe, expect, test} from 'vitest'
import {isObjectNode, isPartialSpanNode, isSpanNode} from './portable-text-node'

const schema = compileSchema(defineSchema({}))

describe(isPartialSpanNode.name, () => {
test('object with only text property', () => {
expect(isPartialSpanNode({schema}, {text: 'Hello'})).toBe(true)
})

test('non-objects', () => {
expect(isPartialSpanNode({schema}, null)).toBe(false)
expect(isPartialSpanNode({schema}, undefined)).toBe(false)
expect(isPartialSpanNode({schema}, 'text')).toBe(false)
expect(isPartialSpanNode({schema}, 123)).toBe(false)
})

test('text is not a string', () => {
expect(isPartialSpanNode({schema}, {text: 123})).toBe(false)
expect(isPartialSpanNode({schema}, {text: null})).toBe(false)
expect(isPartialSpanNode({schema}, {text: undefined})).toBe(false)
})

test('inline object with text field and _type', () => {
expect(
isPartialSpanNode(
{schema},
{_type: 'mention', _key: 'abc123', text: 'John Doe'},
),
).toBe(false)
})

test('block object with text field and _type', () => {
expect(
isPartialSpanNode(
{schema},
{
_type: 'quote',
_key: 'abc123',
text: 'Hello world',
source: 'Anonymous',
},
),
).toBe(false)
})
})

describe(isSpanNode.name, () => {
test('span with _type="span"', () => {
expect(isSpanNode({schema}, {_type: 'span', text: 'Hello'})).toBe(true)
})

test('partial span (no _type, has text)', () => {
expect(isSpanNode({schema}, {text: 'Hello'})).toBe(true)
})

test('object with children', () => {
expect(
isSpanNode({schema}, {_type: 'span', text: 'Hello', children: []}),
).toBe(false)
})

test('object with different _type', () => {
expect(isSpanNode({schema}, {_type: 'mention', text: 'Hello'})).toBe(false)
})
})

describe(isObjectNode.name, () => {
test('inline object', () => {
expect(
isObjectNode(
{schema},
{_type: 'stock-ticker', _key: 'abc', symbol: 'AAPL'},
),
).toBe(true)
})

test('block object', () => {
expect(
isObjectNode(
{schema},
{_type: 'image', _key: 'abc', src: 'https://example.com'},
),
).toBe(true)
})

test('inline object with text field', () => {
expect(
isObjectNode(
{schema},
{_type: 'mention', _key: 'abc123', text: 'John Doe'},
),
).toBe(true)
})

test('block object with text field', () => {
expect(
isObjectNode(
{schema},
{
_type: 'quote',
_key: 'abc123',
text: 'Hello world',
source: 'Anonymous',
},
),
).toBe(true)
})

test('span', () => {
expect(
isObjectNode(
{schema},
{_type: 'span', _key: 'abc', text: 'Hello', marks: []},
),
).toBe(false)
})

test('text block', () => {
expect(
isObjectNode(
{schema},
{
_type: 'block',
_key: 'abc',
children: [{_type: 'span', text: 'Hello'}],
},
),
).toBe(false)
})
})
26 changes: 18 additions & 8 deletions packages/editor/src/internal-utils/portable-text-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,23 @@ export type PartialSpanNode = {
[other: string]: unknown
}

export function isPartialSpanNode(node: unknown): node is PartialSpanNode {
return (
typeof node === 'object' &&
node !== null &&
'text' in node &&
typeof node.text === 'string'
)
export function isPartialSpanNode(
context: {schema: EditorSchema},
node: unknown,
): node is PartialSpanNode {
if (typeof node !== 'object' || node === null) {
return false
}

if (!('text' in node) || typeof node.text !== 'string') {
return false
}

if ('_type' in node && node._type !== context.schema.span.name) {
return false
}

return true
}

//////////
Expand All @@ -104,7 +114,7 @@ export function isObjectNode(
!isEditorNode(node) &&
!isTextBlockNode(context, node) &&
!isSpanNode(context, node) &&
!isPartialSpanNode(node)
!isPartialSpanNode(context, node)
)
}

Expand Down
77 changes: 77 additions & 0 deletions packages/editor/tests/event.child.set.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -395,4 +395,81 @@ describe('event.child.set', () => {
])
})
})

test('Scenario: Setting "text" field on inline object', async () => {
const keyGenerator = createTestKeyGenerator()
const blockKey = keyGenerator()
const mentionKey = keyGenerator()
const initialValue = [
{
_type: 'block',
_key: blockKey,
children: [
{
_type: 'span',
_key: keyGenerator(),
text: '',
marks: [],
},
{
_type: 'mention',
_key: mentionKey,
text: 'J',
},
{
_type: 'span',
_key: keyGenerator(),
text: '',
marks: [],
},
],
style: 'normal',
markDefs: [],
},
]

const {editor} = await createTestEditor({
initialValue,
keyGenerator,
schemaDefinition: defineSchema({
inlineObjects: [
{
name: 'mention',
fields: [{name: 'text', type: 'string'}],
},
],
}),
})

await vi.waitFor(() => {
return expect(getTersePt(editor.getSnapshot().context)).toEqual([
',{mention},',
])
})

editor.send({
type: 'child.set',
at: [{_key: blockKey}, 'children', {_key: mentionKey}],
props: {
text: 'John Doe',
},
})

await vi.waitFor(() => {
return expect(editor.getSnapshot().context.value).toEqual([
{
...initialValue[0],
children: [
initialValue[0]!.children[0],
{
_type: 'mention',
_key: mentionKey,
text: 'John Doe',
},
initialValue[0]!.children[2],
],
},
])
})
})
})
Loading
Loading