diff --git a/demos/src/Examples/NodePos/React/index.html b/demos/src/Examples/NodePos/React/index.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/demos/src/Examples/NodePos/React/index.jsx b/demos/src/Examples/NodePos/React/index.jsx
new file mode 100644
index 00000000000..78e5a552aa4
--- /dev/null
+++ b/demos/src/Examples/NodePos/React/index.jsx
@@ -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: `
+
This is an example document to play around with the NodePos implementation of Tiptap.
+
+ This is a simple paragraph.
+
+
+
+ Here is another paragraph inside this document.
+
+
+ Here we have a paragraph inside a blockquote.
+
+
+ -
+
Unsorted 1
+
+ -
+
Unsorted 2
+
+ -
+
Unsorted 2.1
+
+ -
+
Unsorted 2.2
+
+ -
+
Unsorted 2.3
+
+
+
+ -
+
Unsorted 3
+
+
+
+ -
+
Sorted 1
+
+ -
+
Sorted 2
+
+ -
+
Sorted 2.1
+
+ -
+
Sorted 2.2
+
+ -
+
Sorted 2.3
+
+
+
+ -
+
Sorted 3
+
+
+
+
+ Here we have another paragraph inside a 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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {foundNodes ?
{foundNodes.map(n => (
+
{mapNodePosToString(n)}
+ ))}
: ''}
+
+ )
+}
diff --git a/demos/src/Examples/NodePos/React/index.spec.js b/demos/src/Examples/NodePos/React/index.spec.js
new file mode 100644
index 00000000000..d77241321a4
--- /dev/null
+++ b/demos/src/Examples/NodePos/React/index.spec.js
@@ -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)
+ })
+ })
+})
diff --git a/demos/src/Examples/NodePos/React/styles.scss b/demos/src/Examples/NodePos/React/styles.scss
new file mode 100644
index 00000000000..25ff7049e4a
--- /dev/null
+++ b/demos/src/Examples/NodePos/React/styles.scss
@@ -0,0 +1,15 @@
+/* Basic editor styles */
+.tiptap {
+ > * + * {
+ margin-top: 0.75em;
+ }
+
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
+ line-height: 1.1;
+ }
+}
diff --git a/packages/core/src/NodePos.ts b/packages/core/src/NodePos.ts
index 900f4e39329..60000382ac5 100644
--- a/packages/core/src/NodePos.ts
+++ b/packages/core/src/NodePos.ts
@@ -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) {
@@ -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
}