Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(core): fix nodepos child lookup #5038

Merged
merged 1 commit into from
Apr 9, 2024
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
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
Loading