Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .changeset/fresh-stingrays-learn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tiptap/core': minor
---

Added new `createResizableNodeView` helper function that creates resizable node view elements`
7 changes: 7 additions & 0 deletions .changeset/orange-coins-smoke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@tiptap/core': major
---

the addNodeView function can now return `null` to dynamically disable rendering of a node view

While this should not directly cause any issues, it's noteworthy as it still could affect some behavior in some edge cases.
5 changes: 5 additions & 0 deletions .changeset/perfect-mails-drive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tiptap/extension-image': minor
---

Added a new `resize` option that allows images to be resized. The option adds resize handlers to images allowing users to manually resize images via drag and drop or touch
6 changes: 3 additions & 3 deletions demos/src/Nodes/Image/React/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import React, { useCallback } from 'react'

export default () => {
const editor = useEditor({
extensions: [Document, Paragraph, Text, Image, Dropcursor],
extensions: [Document, Paragraph, Text, Image.configure({ resize: { minWidth: 100, minHeight: 100 } }), Dropcursor],
content: `
<p>This is a basic example of implementing images. Drag to re-order.</p>
<img src="https://placehold.co/800x400" />
<img src="https://placehold.co/800x400/6A00F5/white" />
<img src="https://unsplash.it/seed/tiptap/800/400" />
<img src="https://unsplash.it/seed/tiptap-2/800/400" />
`,
})

Expand Down
37 changes: 37 additions & 0 deletions demos/src/Nodes/Image/React/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,41 @@ context('/src/Nodes/Image/React/', () => {
cy.get('.tiptap').find('img').should('have.attr', 'src', 'foobar.png')
})
})

it('should verify resize handlers on the image node', () => {
// Insert a pre-defined image content to ensure consistent testing
cy.get('.tiptap').then(([{ editor }]) => {
editor.commands.setContent('<img src="test-image.jpg" alt="Test image" />')

// Wait for the image to be properly rendered with its nodeview
cy.wait(500)

// Find the image container with data-node="image" attribute
cy.get('.tiptap [data-node="image"]')
.should('exist')
.and('have.length', 1)
.then($imageNode => {
// Check for all resize handles
const resizeHandles = [
'left',
'right',
'top',
'bottom',
'top-left',
'top-right',
'bottom-left',
'bottom-right',
]

// Verify each resize handle exists with the correct position attribute
resizeHandles.forEach(position => {
cy.wrap($imageNode)
.find(`.resize-handle-${position}`)
.should('exist')
.should('have.attr', 'data-position', position)
.should('be.visible')
})
})
})
})
})
55 changes: 52 additions & 3 deletions demos/src/Nodes/Image/React/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,61 @@

img {
display: block;
height: auto;
}

[data-resize-container] {
&.ProseMirror-selectednode {
background-color: var(--purple-light);
}
}

[data-resize-container] img {
max-width: 100%;
}

[data-resize-container][data-node='image'] {
margin: 1.5rem 0;
max-width: 100%;
}

&.ProseMirror-selectednode {
outline: 3px solid var(--purple);
[data-resize-handle][data-edge] {
background-color: var(--purple);
opacity: 0.8;
transition: opacity 0.2s ease-in-out;

&:hover {
opacity: 1;
}

&[data-orientation='horizontal'] {
width: 4px;
}

&[data-orientation='vertical'] {
height: 4px;
}
}

[data-resize-handle][data-corner] {
background-color: var(--white);
width: 10px;
height: 10px;
border: 2px solid var(--purple);
}

[data-corner='top-left'] {
transform: translate(-50%, -50%);
}

[data-corner='top-right'] {
transform: translate(50%, -50%);
}

[data-corner='bottom-left'] {
transform: translate(-50%, 50%);
}

[data-corner='bottom-right'] {
transform: translate(50%, 50%);
}
}
37 changes: 37 additions & 0 deletions demos/src/Nodes/Image/Vue/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,41 @@ context('/src/Nodes/Image/Vue/', () => {
cy.get('.tiptap').find('img').should('have.attr', 'src', 'foobar.png')
})
})

it('should verify resize handlers on the image node', () => {
// Insert a pre-defined image content to ensure consistent testing
cy.get('.tiptap').then(([{ editor }]) => {
editor.commands.setContent('<img src="test-image.jpg" alt="Test image" />')

// Wait for the image to be properly rendered with its nodeview
cy.wait(500)

// Find the image container with data-node="image" attribute
cy.get('.tiptap [data-node="image"]')
.should('exist')
.and('have.length', 1)
.then($imageNode => {
// Check for all resize handles
const resizeHandles = [
'left',
'right',
'top',
'bottom',
'top-left',
'top-right',
'bottom-left',
'bottom-right',
]

// Verify each resize handle exists with the correct position attribute
resizeHandles.forEach(position => {
cy.wrap($imageNode)
.find(`.resize-handle-${position}`)
.should('exist')
.should('have.attr', 'data-position', position)
.should('be.visible')
})
})
})
})
})
67 changes: 61 additions & 6 deletions demos/src/Nodes/Image/Vue/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,17 @@ export default {

mounted() {
this.editor = new Editor({
extensions: [Document, Paragraph, Text, Image, Dropcursor],
extensions: [
Document,
Paragraph,
Text,
Image.configure({ resize: { minWidth: 100, minHeight: 100 } }),
Dropcursor,
],
content: `
<p>This is a basic example of implementing images. Drag to re-order.</p>
<img src="https://placehold.co/800x400" />
<img src="https://placehold.co/800x400/6A00F5/white" />
<img src="https://unsplash.it/seed/tiptap/800/400" />
<img src="https://unsplash.it/seed/tiptap-2/800/400" />
`,
})
},
Expand All @@ -64,13 +70,62 @@ export default {

img {
display: block;
height: auto;
}

[data-resize-container] {
&.ProseMirror-selectednode {
background-color: var(--purple-light);
}
}

[data-resize-container] img {
max-width: 100%;
}

[data-resize-container][data-node='image'] {
margin: 1.5rem 0;
max-width: 100%;
}

&.ProseMirror-selectednode {
outline: 3px solid var(--purple);
[data-resize-handle][data-edge] {
background-color: var(--purple);
opacity: 0.8;
transition: opacity 0.2s ease-in-out;

&:hover {
opacity: 1;
}

&[data-orientation='horizontal'] {
width: 4px;
}

&[data-orientation='vertical'] {
height: 4px;
}
}

[data-resize-handle][data-corner] {
background-color: var(--white);
width: 10px;
height: 10px;
border: 2px solid var(--purple);
}

[data-corner='top-left'] {
transform: translate(-50%, -50%);
}

[data-corner='top-right'] {
transform: translate(50%, -50%);
}

[data-corner='bottom-left'] {
transform: translate(-50%, 50%);
}

[data-corner='bottom-right'] {
transform: translate(50%, 50%);
}
}
</style>
8 changes: 7 additions & 1 deletion packages/core/src/ExtensionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,10 +206,16 @@ export class ExtensionManager {
return []
}

const nodeViewResult = addNodeView()

if (!nodeViewResult) {
return []
}

const nodeview: NodeViewConstructor = (node, view, getPos, decorations, innerDecorations) => {
const HTMLAttributes = getRenderedAttributes(node, extensionAttributes)

return addNodeView()({
return nodeViewResult({
// pass-through
node,
view,
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface NodeConfig<Options = any, Storage = any>
editor: Editor
type: NodeType
parent: ParentConfig<NodeConfig<Options, Storage>>['addNodeView']
}) => NodeViewRenderer)
}) => NodeViewRenderer | null)
| null

/**
Expand Down
42 changes: 42 additions & 0 deletions packages/core/src/helpers/createResizableNodeView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type {
ResizableNodeViewDiagonalDirection,
ResizableNodeViewDirection,
ResizableNodeViewDirections,
ResizableNodeViewOptions,
} from './resizable/index.js'
import { createResizableNodeView as createResizableNodeViewModular } from './resizable/index.js'

export type {
ResizableNodeViewDiagonalDirection,
ResizableNodeViewDirection,
ResizableNodeViewDirections,
ResizableNodeViewOptions,
}

/**
* Creates a resizable node view for Tiptap
*
* @example
* ```js
* addNodeView() {
* return ({ node, getPos, editor }) => {
* const img = document.createElement('img')
* img.src = node.attrs.src
*
* const resizable = createResizableNodeView({
* dom: img,
* editor,
* getPos,
* node,
* minWidth: 100,
* minHeight: 50,
* })
*
* return { dom: resizable }
* }
* }
* ```
*/
export function createResizableNodeView(options: ResizableNodeViewOptions): HTMLElement {
return createResizableNodeViewModular(options)
}
1 change: 1 addition & 0 deletions packages/core/src/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './combineTransactionSteps.js'
export * from './createChainableState.js'
export * from './createDocument.js'
export * from './createNodeFromContent.js'
export * from './createResizableNodeView.js'
export * from './defaultBlockAt.js'
export * from './findChildren.js'
export * from './findChildrenInRange.js'
Expand Down
Loading
Loading