Skip to content
5 changes: 5 additions & 0 deletions .changeset/cyan-carpets-wonder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tiptap/extension-drag-handle': patch
---

Set drag event dataTransfer when dragging with global drag handle, to allow dragging outside of the editor
5 changes: 5 additions & 0 deletions .changeset/short-steaks-cover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tiptap/core': minor
---

When dragging between editors, allow the user to copy instead of move by holding a modifier key
5 changes: 5 additions & 0 deletions .changeset/sixty-falcons-boil.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tiptap/core': patch
---

Fix bug where dragging between editors may delete extra content from source editor
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { mergeAttributes, Node } from '@tiptap/core'
import { ReactNodeViewRenderer } from '@tiptap/react'

import { RecommendationView } from './views/index.jsx'

export const Recommendation = Node.create({
name: 'recommendation',

group: 'block',

draggable: true,

addOptions() {
return {
publicationId: '',
HTMLAttributes: {
class: `node-${this.name}`,
},
}
},

addAttributes() {
return {
id: {
default: undefined,
parseHTML: element => element.getAttribute('data-id'),
renderHTML: attributes => ({
'data-id': attributes.id,
}),
},
}
},

parseHTML() {
return [
{
tag: `div.node-${this.name}`,
},
]
},

renderHTML({ HTMLAttributes }) {
return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)]
},

addCommands() {
return {
setRecommendation:
() =>
({ chain }) =>
chain()
.focus()
.insertContent({
type: this.name,
})
.run(),
}
},

addNodeView() {
return ReactNodeViewRenderer(RecommendationView)
},
})

export default Recommendation
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Recommendation.jsx'
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { NodeViewWrapper } from '@tiptap/react'

export const RecommendationView = ({ node }) => {
return (
<NodeViewWrapper data-drag-handle>
<div className="title">Recommendation {node.attrs.id}</div>
<p>Test</p>
</NodeViewWrapper>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './RecommendationView.jsx'
Empty file.
53 changes: 53 additions & 0 deletions demos/src/Experiments/GlobalDragHandleMultiEditor/React/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import './styles.scss'

import DragHandle from '@tiptap/extension-drag-handle-react'
import { EditorContent, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'

import { Recommendation } from './extensions/recommendation/index.jsx'

const EditorPane = () => {
const editor = useEditor({
extensions: [StarterKit, Recommendation],
content: `
<h1>
This is a heading.
</h1>
<p>
This is a paragraph.
</p>
<div class="node-recommendation"></div>
<p>
And this one, too.
</p>
`,
})

return (
<div className="editor-wrapper">
<div className="editor-pane">
<DragHandle editor={editor}>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 9h16.5m-16.5 6.75h16.5" />
</svg>
</DragHandle>
<EditorContent editor={editor} />
</div>
</div>
)
}

export default () => {
return (
<div className="editors">
<EditorPane />
<EditorPane />
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
.editors {
display: flex;
flex-direction: row;
}

.editor-wrapper {
width: 50%;
}

.editor-pane {
margin: 1rem;
border: solid 1px black;
}

.ProseMirror {
padding-inline: 4rem;

> * + * {
margin-top: 0.75em;
}

[data-id] {
border: 3px solid #0d0d0d;
border-radius: 0.5rem;
margin: 1rem 0;
position: relative;
margin-top: 1.5rem;
padding: 2rem 1rem 1rem;

&::before {
content: attr(data-id);
background-color: #0d0d0d;
font-size: 0.6rem;
letter-spacing: 1px;
font-weight: bold;
text-transform: uppercase;
color: #fff;
position: absolute;
top: 0;
padding: 0.25rem 0.75rem;
border-radius: 0 0 0.5rem 0.5rem;
}
}
}

.drag-handle {
align-items: center;
background: #f0f0f0;
border-radius: 0.25rem;
border: 1px solid rgba(0, 0, 0, 0.1);
cursor: grab;
display: flex;
height: 1.5rem;
justify-content: center;
width: 1.5rem;

svg {
width: 1.25rem;
height: 1.25rem;
}
}

.node-recommendation {
padding: 0.5rem;
border-radius: 0.5rem;
border: 0.15rem solid #000;

.title {
font-size: 0.875rem;
color: #777;
}

p {
margin: 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { NodeViewContent, NodeViewWrapper } from '@tiptap/react'
import React from 'react'

export default () => {
return (
<NodeViewWrapper className="draggable-item">
<div className="drag-handle" contentEditable={false} draggable="true" data-drag-handle />
<NodeViewContent className="content" />
</NodeViewWrapper>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { mergeAttributes, Node } from '@tiptap/core'
import { ReactNodeViewRenderer } from '@tiptap/react'

import Component from './Component.jsx'

export default Node.create({
name: 'draggableItem',

group: 'block',

content: 'block+',

draggable: true,

parseHTML() {
return [
{
tag: 'div[data-type="draggable-item"]',
},
]
},

renderHTML({ HTMLAttributes }) {
return ['div', mergeAttributes(HTMLAttributes, { 'data-type': 'draggable-item' }), 0]
},

addNodeView() {
return ReactNodeViewRenderer(Component)
},
})
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import './styles.scss'

import { EditorProvider } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'

import DraggableItem from './DraggableItem.js'

const extensions = [StarterKit, DraggableItem]

const content = `
<p>This is a boring paragraph.</p>
<div data-type="draggable-item">
<p>Followed by a fancy draggable item.</p>
</div>
<div data-type="draggable-item">
<p>And another draggable item.</p>
<div data-type="draggable-item">
<p>And a nested one.</p>
<div data-type="draggable-item">
<p>But can we go deeper?</p>
</div>
</div>
</div>
<p>Let’s finish with a boring paragraph.</p>
`

const EditorPane = () => {
return (
<div className="editor-wrapper">
<div className="editor-pane">
<EditorProvider extensions={extensions} content={content}></EditorProvider>
</div>
</div>
)
}

export default () => {
return (
<div className="editors">
<EditorPane />
<EditorPane />
</div>
)
}
Loading
Loading