Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
10 changes: 10 additions & 0 deletions .changeset/ready-rings-fall.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@tiptap/extension-drag-handle': patch
---

Replace DOM traversal with browser's native elementsFromPoint for better performance.

- Use elementsFromPoint instead of querySelectorAll
- Add clampToContent helper for coordinate boundary validation
- Add findClosestTopLevelBlock helper for efficient block lookup
- Future-proof for root-level mousemove listeners
140 changes: 47 additions & 93 deletions demos/src/Extensions/DragHandle/Vue/index.vue
Original file line number Diff line number Diff line change
@@ -1,20 +1,10 @@
<template>
<div v-if="editor">
<button @click="editor.chain().focus().toggleHeading({ level: 1 }).run()">H1</button>
<button @click="editor.chain().focus().toggleHeading({ level: 2 }).run()">H2</button>
<button @click="editor.chain().focus().toggleBold().run()">Bold</button>
<button
@click="editor.chain().focus().toggleBulletList().run()"
:class="{ 'is-active': editor.isActive('bulletList') }"
>
Bullet list
</button>
<button @click="editor.chain().focus().lockDragHandle().run()">Lock drag handle</button>
<button @click="editor.chain().focus().unlockDragHandle().run()">Unlock drag handle</button>
<button @click="editor.chain().focus().toggleDragHandle().run()">Toggle drag handle</button>
<button @click="editor.setEditable(!editor.isEditable)">Toggle editable</button>
<drag-handle :editor="editor">
<div class="custom-drag-handle" />
<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>
</drag-handle>
</div>

Expand All @@ -23,7 +13,6 @@

<script>
import { DragHandle } from '@tiptap/extension-drag-handle-vue-3'
import NodeRange from '@tiptap/extension-node-range'
import StarterKit from '@tiptap/starter-kit'
import { Editor, EditorContent } from '@tiptap/vue-3'

Expand All @@ -39,31 +28,17 @@ export default {
},
mounted() {
this.editor = new Editor({
extensions: [
StarterKit,
NodeRange.configure({
// allow to select only on depth 0
// depth: 0,
key: null,
}),
],
extensions: [StarterKit],
content: `
<h1>This is a demo file for our Drag Handle extension experiement.</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ipsum suspendisse ultrices gravida dictum fusce ut placerat. Viverra mauris in aliquam sem fringilla. Sit amet commodo nulla facilisi nullam. Viverra orci sagittis eu volutpat odio facilisis mauris sit. In hendrerit gravida rutrum quisque non tellus orci ac. Pellentesque adipiscing commodo elit at imperdiet. Pulvinar sapien et ligula ullamcorper malesuada proin. Odio pellentesque diam volutpat commodo. Pharetra diam sit amet nisl suscipit adipiscing bibendum est ultricies.</p>
<p>Odio eu feugiat pretium nibh ipsum consequat nisl. Velit euismod in pellentesque massa placerat. Vel quam elementum pulvinar etiam non quam. Sit amet purus gravida quis. Tincidunt eget nullam non nisi est sit. Eget nulla facilisi etiam dignissim diam. Magnis dis parturient montes nascetur ridiculus mus mauris vitae. Vitae congue eu consequat ac felis donec et odio pellentesque. Sit amet porttitor eget dolor morbi non arcu risus quis. Suspendisse ultrices gravida dictum fusce ut. Tortor vitae purus faucibus ornare. Faucibus ornare suspendisse sed nisi lacus sed. Tristique senectus et netus et.</p>
<p>Cursus euismod quis viverra nibh cras pulvinar mattis nunc. Sem viverra aliquet eget sit amet tellus. Nec ullamcorper sit amet risus nullam. Facilisis gravida neque convallis a cras semper auctor. Habitant morbi tristique senectus et netus et malesuada fames ac. Dui vivamus arcu felis bibendum. Velit laoreet id donec ultrices. Enim diam vulputate ut pharetra sit. Aenean pharetra magna ac placerat vestibulum lectus mauris. Mi eget mauris pharetra et ultrices. Lacus viverra vitae congue eu consequat ac felis donec.</p>
<h2></h2>
<p>Odio eu feugiat pretium nibh ipsum consequat nisl. Velit euismod in pellentesque massa placerat. Vel quam elementum pulvinar etiam non quam. Sit amet purus gravida quis. Tincidunt eget nullam non nisi est sit. Eget nulla facilisi etiam dignissim diam. Magnis dis parturient montes nascetur ridiculus mus mauris vitae. Vitae congue eu consequat ac felis donec et odio pellentesque. Sit amet porttitor eget dolor morbi non arcu risus quis. Suspendisse ultrices gravida dictum fusce ut. Tortor vitae purus faucibus ornare. Faucibus ornare suspendisse sed nisi lacus sed. Tristique senectus et netus et.</p>
<p>Cursus euismod quis viverra nibh cras pulvinar mattis nunc. Sem viverra aliquet eget sit amet tellus. Nec ullamcorper sit amet risus nullam. Facilisis gravida neque convallis a cras semper auctor. Habitant morbi tristique senectus et netus et malesuada fames ac. Dui vivamus arcu felis bibendum. Velit laoreet id donec ultrices. Enim diam vulputate ut pharetra sit. Aenean pharetra magna ac placerat vestibulum lectus mauris. Mi eget mauris pharetra et ultrices. Lacus viverra vitae congue eu consequat ac felis donec.</p>
<ul>
<li>Bullet Item 1</li>
<li>Bullet Item 2</li>
<li>Bullet Item 3</li>
</ul>
<h2>Lorem Ipsum</h2>
<p>Tincidunt ornare massa eget egestas. Neque convallis a cras semper auctor neque. Eget nulla facilisi etiam dignissim diam quis enim. Phasellus vestibulum lorem sed risus ultricies tristique nulla aliquet enim. At tempor commodo ullamcorper a lacus vestibulum sed arcu. Sed vulputate mi sit amet mauris commodo quis imperdiet. Eget gravida cum sociis natoque. Lacinia quis vel eros donec ac odio tempor orci dapibus. Integer vitae justo eget magna fermentum iaculis eu non. Sed odio morbi quis commodo. Neque sodales ut etiam sit amet. Ipsum nunc aliquet bibendum enim facilisis gravida neque convallis a. Tempus quam pellentesque nec nam aliquam sem et tortor consequat. Urna nec tincidunt praesent semper feugiat nibh sed pulvinar proin. Lacus sed turpis tincidunt id aliquet risus feugiat in. Et leo duis ut diam quam nulla. Ultrices eros in cursus turpis. Adipiscing elit ut aliquam purus sit amet luctus venenatis.</p>
<h3>Lorem Ipsum</h3>
<p>Sapien eget mi proin sed libero enim sed faucibus. Aliquam id diam maecenas ultricies mi eget mauris. Amet mattis vulputate enim nulla aliquet porttitor lacus. Pulvinar elementum integer enim neque volutpat ac. Libero volutpat sed cras ornare arcu dui vivamus arcu felis. Urna nunc id cursus metus aliquam eleifend mi in nulla. Justo laoreet sit amet cursus sit. In massa tempor nec feugiat nisl pretium fusce. Vel quam elementum pulvinar etiam non. Nisl nisi scelerisque eu ultrices vitae. Odio ut enim blandit volutpat maecenas volutpat blandit aliquam.</p>
<h1>
This is a very unique heading.
</h1>
<p>
This is a unique paragraph. It’s so unique, it even has an ID attached to it.
</p>
<p>
And this one, too.
</p>
`,
})
},
Expand All @@ -74,72 +49,51 @@ export default {
</script>

<style lang="scss">
::selection {
background-color: #70cff850;
}

.ProseMirror {
padding: 1rem 1rem 1rem 0;
padding-inline: 4rem;

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

> * {
margin-left: 3rem;
}

.ProseMirror-widget * {
margin-top: auto;
}

ul,
ol {
padding: 0 1rem;
}
}

.ProseMirror-noderangeselection {
*::selection {
background: transparent;
}
[data-id] {
border: 3px solid #0d0d0d;
border-radius: 0.5rem;
margin: 1rem 0;
position: relative;
margin-top: 1.5rem;
padding: 2rem 1rem 1rem;

* {
caret-color: transparent;
&::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;
}
}
}

.ProseMirror-selectednode,
.ProseMirror-selectednoderange {
position: relative;

&::before {
position: absolute;
pointer-events: none;
z-index: -1;
content: '';
top: -0.25rem;
left: -0.25rem;
right: -0.25rem;
bottom: -0.25rem;
background-color: #70cff850;
border-radius: 0.2rem;
}
}
.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;

.custom-drag-handle {
&::after {
display: flex;
align-items: center;
justify-content: center;
width: 1rem;
svg {
width: 1.25rem;
height: 1.25rem;
content: '⠿';
font-weight: 700;
cursor: grab;
background: #0d0d0d10;
color: #0d0d0d50;
border-radius: 0.25rem;
}
}
</style>
66 changes: 50 additions & 16 deletions packages/extension-drag-handle-vue-3/src/DragHandle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,27 @@ import {
DragHandlePlugin,
dragHandlePluginDefaultKey,
} from '@tiptap/extension-drag-handle'
import type { Node } from '@tiptap/pm/model'
import type { Plugin, PluginKey } from '@tiptap/pm/state'
import type { Editor } from '@tiptap/vue-3'
import type { PropType } from 'vue'
import { defineComponent, h, onBeforeUnmount, onMounted, ref } from 'vue'
import { defineComponent, h, nextTick, onBeforeUnmount, onMounted, ref, shallowRef } from 'vue'

type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>

export type DragHandleProps = Omit<Optional<DragHandlePluginProps, 'pluginKey'>, 'element'> & {
class?: string
onNodeChange?: (data: { node: Node | null; editor: Editor; pos: number }) => void
onElementDragStart?: DragHandlePluginProps['onElementDragStart']
onElementDragEnd?: DragHandlePluginProps['onElementDragEnd']
}

export const DragHandle = defineComponent({
name: 'DragHandleVue',

props: {
pluginKey: {
type: [String, Object] as PropType<DragHandleProps['pluginKey']>,
type: [String, Object] as PropType<PluginKey | string>,
default: dragHandlePluginDefaultKey,
},

Expand All @@ -39,6 +43,16 @@ export const DragHandle = defineComponent({
default: null,
},

onElementDragStart: {
type: Function as PropType<DragHandleProps['onElementDragStart']>,
default: null,
},

onElementDragEnd: {
type: Function as PropType<DragHandleProps['onElementDragEnd']>,
default: null,
},

class: {
type: String as PropType<DragHandleProps['class']>,
default: 'drag-handle',
Expand All @@ -47,25 +61,45 @@ export const DragHandle = defineComponent({

setup(props, { slots }) {
const root = ref<HTMLElement | null>(null)
const pluginHandle = shallowRef<{ plugin: Plugin; unbind: () => void } | null>(null)

onMounted(() => {
const { editor, pluginKey, onNodeChange, computePositionConfig } = props

editor.registerPlugin(
DragHandlePlugin({
editor,
element: root.value as HTMLElement,
pluginKey,
computePositionConfig: { ...defaultComputePositionConfig, ...computePositionConfig },
onNodeChange,
}).plugin,
)
onMounted(async () => {
await nextTick()

const { editor, pluginKey, onNodeChange, onElementDragEnd, onElementDragStart, computePositionConfig } = props

if (!root.value) {
return
}
if (!props.editor || props.editor.isDestroyed) {
return
}

const init = DragHandlePlugin({
editor,
element: root.value,
pluginKey,
computePositionConfig: { ...defaultComputePositionConfig, ...computePositionConfig },
onNodeChange,
onElementDragStart,
onElementDragEnd,
})

pluginHandle.value = init
props.editor.registerPlugin(init.plugin)
})

onBeforeUnmount(() => {
const { pluginKey, editor } = props
if (!pluginHandle.value) {
return
}

if (props.editor && !props.editor.isDestroyed) {
props.editor.unregisterPlugin(props.pluginKey)
}

editor.unregisterPlugin(pluginKey as string)
pluginHandle.value.unbind?.()
pluginHandle.value = null
})

return () =>
Expand Down
Loading
Loading