From 3ddd52dfec5b81d0e6f090e31a32dc6e8bef28a2 Mon Sep 17 00:00:00 2001 From: bdbch Date: Sat, 6 Apr 2024 18:59:16 +0200 Subject: [PATCH] fix(core): fix nodepos child lookup --- demos/src/Examples/NodePos/React/index.html | 0 demos/src/Examples/NodePos/React/index.jsx | 251 ++++++++++++++++++ .../src/Examples/NodePos/React/index.spec.js | 103 +++++++ demos/src/Examples/NodePos/React/styles.scss | 15 ++ packages/core/src/NodePos.ts | 4 +- 5 files changed, 371 insertions(+), 2 deletions(-) create mode 100644 demos/src/Examples/NodePos/React/index.html create mode 100644 demos/src/Examples/NodePos/React/index.jsx create mode 100644 demos/src/Examples/NodePos/React/index.spec.js create mode 100644 demos/src/Examples/NodePos/React/styles.scss 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. +

+ A 200x200 thumbnail from unsplash. +

+ Here is another paragraph inside this document. +

+
+

Here we have a paragraph inside a blockquote.

+
+ +
    +
  1. +

    Sorted 1

    +
  2. +
  3. +

    Sorted 2

    +
      +
    • +

      Sorted 2.1

      +
    • +
    • +

      Sorted 2.2

      +
    • +
    • +

      Sorted 2.3

      +
    • +
    +
  4. +
  5. +

    Sorted 3

    +
  6. +
+ A 260x200 thumbnail from unsplash. +
+

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 }