Skip to content

Commit

Permalink
fix(core): fix nodepos child lookup (#5038)
Browse files Browse the repository at this point in the history
  • Loading branch information
bdbch authored Apr 9, 2024
1 parent 96b6abc commit 22ced31
Show file tree
Hide file tree
Showing 5 changed files with 371 additions and 2 deletions.
Empty file.
251 changes: 251 additions & 0 deletions demos/src/Examples/NodePos/React/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
import './styles.scss'

import Image from '@tiptap/extension-image'
import { EditorContent, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import React, { useCallback, useState } from 'react'

const mapNodePosToString = nodePos => `[${nodePos.node.type.name} ${nodePos.range.from}-${nodePos.range.to}] ${nodePos.textContent} | ${JSON.stringify(nodePos.node.attrs)}`

export default () => {
const editor = useEditor({
extensions: [
StarterKit,
Image,
],
content: `
<h1>This is an example document to play around with the NodePos implementation of Tiptap.</h1>
<p>
This is a <strong>simple</strong> paragraph.
</p>
<img src="https://unsplash.it/200/200" alt="A 200x200 thumbnail from unsplash." />
<p>
Here is another paragraph inside this document.
</p>
<blockquote>
<p>Here we have a paragraph inside a blockquote.</p>
</blockquote>
<ul>
<li>
<p>Unsorted 1</p>
</li>
<li>
<p>Unsorted 2</p>
<ul>
<li>
<p>Unsorted 2.1</p>
</li>
<li>
<p>Unsorted 2.2</p>
</li>
<li>
<p>Unsorted 2.3</p>
</li>
</ul>
</li>
<li>
<p>Unsorted 3</p>
</li>
</ul>
<ol>
<li>
<p>Sorted 1</p>
</li>
<li>
<p>Sorted 2</p>
<ul>
<li>
<p>Sorted 2.1</p>
</li>
<li>
<p>Sorted 2.2</p>
</li>
<li>
<p>Sorted 2.3</p>
</li>
</ul>
</li>
<li>
<p>Sorted 3</p>
</li>
</ol>
<img src="https://unsplash.it/260/200" alt="A 260x200 thumbnail from unsplash." />
<blockquote>
<p>Here we have another paragraph inside a blockquote.</p>
</blockquote>
`,
})

const [foundNodes, setFoundNodes] = useState(null)

const findParagraphs = useCallback(() => {
const nodePositions = editor.$doc.querySelectorAll('paragraph')

if (!nodePositions) {
setFoundNodes(null)
return
}

setFoundNodes(nodePositions)
}, [editor])

const findListItems = useCallback(() => {
const nodePositions = editor.$doc.querySelectorAll('listItem')

if (!nodePositions) {
setFoundNodes(null)
return
}

setFoundNodes(nodePositions)
}, [editor])

const findBulletList = useCallback(() => {
const nodePositions = editor.$doc.querySelectorAll('bulletList')

if (!nodePositions) {
setFoundNodes(null)
return
}

setFoundNodes(nodePositions)
}, [editor])

const findOrderedList = useCallback(() => {
const nodePositions = editor.$doc.querySelectorAll('orderedList')

if (!nodePositions) {
setFoundNodes(null)
return
}

setFoundNodes(nodePositions)
}, [editor])

const findBlockquote = useCallback(() => {
const nodePositions = editor.$doc.querySelectorAll('blockquote')

if (!nodePositions) {
setFoundNodes(null)
return
}

setFoundNodes(nodePositions)
}, [editor])

const findImages = useCallback(() => {
const nodePositions = editor.$doc.querySelectorAll('image')

if (!nodePositions) {
setFoundNodes(null)
return
}

setFoundNodes(nodePositions)
}, [editor])

const findFirstBlockquote = useCallback(() => {
const nodePosition = editor.$doc.querySelector('blockquote')

if (!nodePosition) {
setFoundNodes(null)
return
}

setFoundNodes([nodePosition])
}, [editor])

const findSquaredImage = useCallback(() => {
const nodePosition = editor.$doc.querySelector('image', { src: 'https://unsplash.it/200/200' })

if (!nodePosition) {
setFoundNodes(null)
return
}

setFoundNodes([nodePosition])
}, [editor])

const findLandscapeImage = useCallback(() => {
const nodePosition = editor.$doc.querySelector('image', { src: 'https://unsplash.it/260/200' })

if (!nodePosition) {
setFoundNodes(null)
return
}

setFoundNodes([nodePosition])
}, [editor])

const findFirstNode = useCallback(() => {
const nodePosition = editor.$doc.firstChild

if (!nodePosition) {
setFoundNodes(null)
return
}

setFoundNodes([nodePosition])
}, [editor])

const findLastNode = useCallback(() => {
const nodePosition = editor.$doc.lastChild

if (!nodePosition) {
setFoundNodes(null)
return
}

setFoundNodes([nodePosition])
}, [editor])

const findLastNodeOfFirstBulletList = useCallback(() => {
const nodePosition = editor.$doc.querySelector('bulletList').lastChild

if (!nodePosition) {
setFoundNodes(null)
return
}

setFoundNodes([nodePosition])
}, [editor])

const findNonexistentNode = useCallback(() => {
const nodePosition = editor.$doc.querySelector('nonexistent')

if (!nodePosition) {
setFoundNodes(null)
return
}

setFoundNodes([nodePosition])
}, [editor])

return (
<div>
<div>
<button data-testid="find-paragraphs" onClick={findParagraphs}>Find paragraphs</button>
<button data-testid="find-listitems" onClick={findListItems}>Find list items</button>
<button data-testid="find-bulletlists" onClick={findBulletList}>Find bullet lists</button>
<button data-testid="find-orderedlists" onClick={findOrderedList}>Find ordered lists</button>
<button data-testid="find-blockquotes" onClick={findBlockquote}>Find blockquotes</button>
<button data-testid="find-images" onClick={findImages}>Find images</button>
</div>
<div>
<button data-testid="find-first-blockquote" onClick={findFirstBlockquote}>Find first blockquote</button>
<button data-testid="find-squared-image" onClick={findSquaredImage}>Find squared image</button>
<button data-testid="find-landscape-image" onClick={findLandscapeImage}>Find landscape image</button>
</div>
<div>
<button data-testid="find-first-node" onClick={findFirstNode}>Find first node</button>
<button data-testid="find-last-node" onClick={findLastNode}>Find last node</button>
<button data-testid="find-last-node-of-first-bullet-list" onClick={findLastNodeOfFirstBulletList}>Find last node of first bullet list</button>
<button data-testid="find-nonexistent-node" onClick={findNonexistentNode}>Find nonexistent node</button>
</div>
<EditorContent editor={editor} />
{foundNodes ? <div data-testid="found-nodes">{foundNodes.map(n => (
<div data-testid="found-node" key={n.pos}>{mapNodePosToString(n)}</div>
))}</div> : ''}
</div>
)
}
103 changes: 103 additions & 0 deletions demos/src/Examples/NodePos/React/index.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
context('/src/Examples/NodePos/React/', () => {
beforeEach(() => {
cy.visit('/src/Examples/NodePos/React/')
})

it('should get paragraphs', () => {
cy.get('.tiptap').then(() => {
cy.get('button[data-testid="find-paragraphs"]').click()
cy.get('div[data-testid="found-nodes"]').should('exist')
cy.get('div[data-testid="found-node"]').should('have.length', 16)
})
})

it('should get list items', () => {
cy.get('.tiptap').then(() => {
cy.get('button[data-testid="find-listitems"]').click()
cy.get('div[data-testid="found-nodes"]').should('exist')
cy.get('div[data-testid="found-node"]').should('have.length', 12)
})
})

it('should get bullet lists', () => {
cy.get('.tiptap').then(() => {
cy.get('button[data-testid="find-bulletlists"]').click()
cy.get('div[data-testid="found-nodes"]').should('exist')
cy.get('div[data-testid="found-node"]').should('have.length', 3)
})
})

it('should get ordered lists', () => {
cy.get('.tiptap').then(() => {
cy.get('button[data-testid="find-orderedlists"]').click()
cy.get('div[data-testid="found-nodes"]').should('exist')
cy.get('div[data-testid="found-node"]').should('have.length', 1)
})
})

it('should get blockquotes', () => {
cy.get('.tiptap').then(() => {
cy.get('button[data-testid="find-blockquotes"]').click()
cy.get('div[data-testid="found-nodes"]').should('exist')
cy.get('div[data-testid="found-node"]').should('have.length', 2)
})
})

it('should get images', () => {
cy.get('.tiptap').then(() => {
cy.get('button[data-testid="find-images"]').click()
cy.get('div[data-testid="found-nodes"]').should('exist')
cy.get('div[data-testid="found-node"]').should('have.length', 2)
})
})

it('should get first blockquote', () => {
cy.get('.tiptap').then(() => {
cy.get('button[data-testid="find-first-blockquote"]').click()
cy.get('div[data-testid="found-nodes"]').should('exist')
cy.get('div[data-testid="found-node"]').should('have.length', 1)
cy.get('div[data-testid="found-node"]').should('contain', 'Here we have a paragraph inside a blockquote.').should('not.contain', 'Here we have another paragraph inside a blockquote.')
})
})

it('should get images by attributes', () => {
cy.get('.tiptap').then(() => {
cy.get('button[data-testid="find-squared-image"]').click()
cy.get('div[data-testid="found-nodes"]').should('exist')
cy.get('div[data-testid="found-node"]').should('have.length', 1)
cy.get('div[data-testid="found-node"]').should('contain', 'https://unsplash.it/200/200')

cy.get('button[data-testid="find-landscape-image"]').click()
cy.get('div[data-testid="found-nodes"]').should('exist')
cy.get('div[data-testid="found-node"]').should('have.length', 1)
cy.get('div[data-testid="found-node"]').should('contain', 'https://unsplash.it/260/200')
})
})

it('should find complex nodes', () => {
cy.get('.tiptap').then(() => {
cy.get('button[data-testid="find-first-node"]').click()
cy.get('div[data-testid="found-nodes"]').should('exist')
cy.get('div[data-testid="found-node"]').should('have.length', 1)
cy.get('div[data-testid="found-node"]').should('contain', 'heading').should('contain', '{"level":1}')

cy.get('button[data-testid="find-last-node"]').click()
cy.get('div[data-testid="found-nodes"]').should('exist')
cy.get('div[data-testid="found-node"]').should('have.length', 1)
cy.get('div[data-testid="found-node"]').should('contain', 'blockquote')

cy.get('button[data-testid="find-last-node-of-first-bullet-list"]').click()
cy.get('div[data-testid="found-nodes"]').should('exist')
cy.get('div[data-testid="found-node"]').should('have.length', 1)
cy.get('div[data-testid="found-node"]').should('contain', 'listItem').should('contain', 'Unsorted 3')
})
})

it('should not find nodes that do not exist in document', () => {
cy.get('.tiptap').then(() => {
cy.get('button[data-testid="find-nonexistent-node"]').click()
cy.get('div[data-testid="found-nodes"]').should('not.exist')
cy.get('div[data-testid="found-node"]').should('have.length', 0)
})
})
})
15 changes: 15 additions & 0 deletions demos/src/Examples/NodePos/React/styles.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/* Basic editor styles */
.tiptap {
> * + * {
margin-top: 0.75em;
}

h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
}
}
4 changes: 2 additions & 2 deletions packages/core/src/NodePos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export class NodePos {
this.node.content.forEach((node, offset) => {
const isBlock = node.isBlock && !node.isTextblock

const targetPos = this.pos + offset + (isBlock ? 0 : 1)
const targetPos = this.pos + offset + 1
const $pos = this.resolvedPos.doc.resolve(targetPos)

if (!isBlock && $pos.depth <= this.depth) {
Expand Down Expand Up @@ -201,7 +201,7 @@ export class NodePos {
let nodes: NodePos[] = []

// iterate through children recursively finding all nodes which match the selector with the node name
if (this.isBlock || !this.children || this.children.length === 0) {
if (!this.children || this.children.length === 0) {
return nodes
}

Expand Down

0 comments on commit 22ced31

Please sign in to comment.