generated from template/vite-react-template
update temp
This commit is contained in:
77
package.json
77
package.json
@@ -23,36 +23,39 @@
|
||||
"author": "abearxiong <xiongxiao@xiongxiao.me>",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@kevisual/router": "0.0.10",
|
||||
"@tiptap/core": "^2.11.7",
|
||||
"@tiptap/extension-code-block-lowlight": "^2.11.7",
|
||||
"@tiptap/extension-document": "^2.11.7",
|
||||
"@tiptap/extension-highlight": "^2.11.7",
|
||||
"@tiptap/extension-paragraph": "^2.11.7",
|
||||
"@tiptap/extension-placeholder": "^2.11.7",
|
||||
"@tiptap/extension-text": "^2.11.7",
|
||||
"@tiptap/extension-typography": "^2.11.7",
|
||||
"@tiptap/pm": "^2.11.7",
|
||||
"@tiptap/starter-kit": "^2.11.7",
|
||||
"@tiptap/suggestion": "^2.11.7",
|
||||
"@kevisual/router": "0.0.30",
|
||||
"@tiptap/core": "^3.8.0",
|
||||
"@tiptap/extension-code-block-lowlight": "^3.8.0",
|
||||
"@tiptap/extension-document": "^3.8.0",
|
||||
"@tiptap/extension-drag-handle-react": "^3.8.0",
|
||||
"@tiptap/extension-dropcursor": "^3.8.0",
|
||||
"@tiptap/extension-gapcursor": "^3.8.0",
|
||||
"@tiptap/extension-highlight": "^3.8.0",
|
||||
"@tiptap/extension-paragraph": "^3.8.0",
|
||||
"@tiptap/extension-placeholder": "^3.8.0",
|
||||
"@tiptap/extension-text": "^3.8.0",
|
||||
"@tiptap/extension-typography": "^3.8.0",
|
||||
"@tiptap/pm": "^3.8.0",
|
||||
"@tiptap/starter-kit": "^3.8.0",
|
||||
"@tiptap/suggestion": "^3.8.0",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"dayjs": "^1.11.18",
|
||||
"github-markdown-css": "^5.8.1",
|
||||
"highlight.js": "^11.11.1",
|
||||
"idb": "^8.0.2",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"immer": "^10.1.1",
|
||||
"idb": "^8.0.3",
|
||||
"idb-keyval": "^6.2.2",
|
||||
"immer": "^10.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lowlight": "^3.3.0",
|
||||
"lucide-react": "^0.487.0",
|
||||
"marked": "^15.0.7",
|
||||
"nanoid": "^5.1.5",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"lucide-react": "^0.548.0",
|
||||
"marked": "^16.4.1",
|
||||
"nanoid": "^5.1.6",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-toastify": "^11.0.5",
|
||||
"tiptap-markdown": "^0.8.10",
|
||||
"turndown": "^7.2.0",
|
||||
"zustand": "^5.0.3"
|
||||
"tiptap-markdown": "^0.9.0",
|
||||
"turndown": "^7.2.2",
|
||||
"zustand": "^5.0.8"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.tsx",
|
||||
@@ -62,18 +65,18 @@
|
||||
"access": "public"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kevisual/query": "0.0.15",
|
||||
"@kevisual/types": "^0.0.6",
|
||||
"@tailwindcss/vite": "^4.1.3",
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/react": "^19.1.0",
|
||||
"@types/react-dom": "^19.1.1",
|
||||
"@types/turndown": "^5.0.5",
|
||||
"@vitejs/plugin-basic-ssl": "^2.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"tailwindcss": "^4.1.3",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.2.5"
|
||||
"@kevisual/query": "0.0.29",
|
||||
"@kevisual/types": "^0.0.10",
|
||||
"@tailwindcss/vite": "^4.1.16",
|
||||
"@types/node": "^24.9.1",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"@types/turndown": "^5.0.6",
|
||||
"@vitejs/plugin-basic-ssl": "^2.1.0",
|
||||
"@vitejs/plugin-react": "^5.1.0",
|
||||
"tailwindcss": "^4.1.16",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.1.12"
|
||||
},
|
||||
"packageManager": "pnpm@10.7.1"
|
||||
"packageManager": "pnpm@10.19.0"
|
||||
}
|
||||
2375
pnpm-lock.yaml
generated
2375
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
202
public/drag-demo.html
Normal file
202
public/drag-demo.html
Normal file
@@ -0,0 +1,202 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Tiptap 拖拽换行演示</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.demo-container {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.instructions h3 {
|
||||
margin-top: 0;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.instructions ul {
|
||||
margin: 8px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.instructions li {
|
||||
margin: 4px 0;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
/* 编辑器样式 */
|
||||
.tiptap {
|
||||
min-height: 300px;
|
||||
padding: 16px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
background: white;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.tiptap:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
/* 拖拽相关样式 */
|
||||
.tiptap .ProseMirror-dropcursor {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
height: 1.2em;
|
||||
width: 2px;
|
||||
background-color: #3b82f6;
|
||||
margin-left: -1px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.tiptap .ProseMirror-gapcursor {
|
||||
display: none;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.tiptap .ProseMirror-gapcursor:after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
width: 20px;
|
||||
border-top: 1px solid #3b82f6;
|
||||
animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite;
|
||||
}
|
||||
|
||||
@keyframes ProseMirror-cursor-blink {
|
||||
to {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.tiptap .ProseMirror-selectednode {
|
||||
outline: 2px solid #3b82f6;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* 可拖拽块的样式 */
|
||||
.tiptap .draggable-block {
|
||||
position: relative;
|
||||
cursor: grab;
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tiptap .draggable-block:hover {
|
||||
background-color: rgba(59, 130, 246, 0.05);
|
||||
padding: 4px;
|
||||
margin: -4px;
|
||||
}
|
||||
|
||||
.tiptap .draggable-block:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* 拖拽状态下的样式 */
|
||||
.tiptap .dragging {
|
||||
opacity: 0.5;
|
||||
transform: rotate(2deg);
|
||||
background-color: rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.tiptap .drop-target {
|
||||
background-color: rgba(59, 130, 246, 0.1);
|
||||
border: 2px dashed #3b82f6;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
/* 段落样式 */
|
||||
.tiptap p {
|
||||
margin: 0.75em 0;
|
||||
}
|
||||
|
||||
.tiptap h1, .tiptap h2, .tiptap h3 {
|
||||
margin: 1.5em 0 0.5em 0;
|
||||
}
|
||||
|
||||
.tiptap ul, .tiptap ol {
|
||||
padding-left: 1.5em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Tiptap 多行拖拽换行演示</h1>
|
||||
|
||||
<div class="instructions">
|
||||
<h3>🎯 使用说明</h3>
|
||||
<ul>
|
||||
<li><strong>拖拽段落:</strong> 将鼠标悬停在任何段落上,然后拖拽它到新位置</li>
|
||||
<li><strong>拖拽标题:</strong> 标题也可以拖拽重新排序</li>
|
||||
<li><strong>拖拽列表项:</strong> 列表项可以在列表内部或列表之间拖拽</li>
|
||||
<li><strong>视觉反馈:</strong> 拖拽时会有蓝色指示线显示插入位置</li>
|
||||
<li><strong>多行支持:</strong> 长段落会保持完整性,整个段落一起移动</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="demo-container">
|
||||
<div id="editor"></div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { TextEditor } from './src/tiptap/editor.ts';
|
||||
|
||||
// 初始化编辑器
|
||||
const editor = new TextEditor();
|
||||
const editorElement = document.getElementById('editor');
|
||||
|
||||
const initialContent = `
|
||||
<h2>欢迎使用拖拽编辑器</h2>
|
||||
<p>这是第一个段落。你可以尝试拖拽这个段落到其他位置。鼠标悬停时段落会有高亮效果,然后你就可以拖拽它了。</p>
|
||||
<p>这是第二个段落,包含更多文本。这个段落演示了如何拖拽包含多行文本的段落。无论段落有多长,整个段落都会作为一个整体进行移动。</p>
|
||||
<h3>功能特点</h3>
|
||||
<ul>
|
||||
<li>支持段落拖拽重排</li>
|
||||
<li>支持标题拖拽</li>
|
||||
<li>支持列表项拖拽</li>
|
||||
<li>实时视觉反馈</li>
|
||||
</ul>
|
||||
<p>这是一个较长的段落,用来演示多行文本的拖拽功能。当你拖拽这个段落时,整个段落内容都会一起移动,包括所有的文本和格式。这样确保了内容的完整性和编辑的便利性。</p>
|
||||
<h3>试试拖拽功能</h3>
|
||||
<p>试着拖拽上面的任何段落或标题到新的位置。你会看到一个蓝色的指示线,显示内容将被插入的位置。</p>
|
||||
<p>最后一个段落。你可以将这个段落拖拽到文档的任何位置。</p>
|
||||
`;
|
||||
|
||||
editor.createEditor(editorElement, {
|
||||
html: initialContent,
|
||||
placeholder: '开始输入,或拖拽现有内容来重新排序...',
|
||||
onUpdateHtml: (html) => {
|
||||
console.log('内容更新:', html);
|
||||
}
|
||||
});
|
||||
|
||||
// 确保编辑器获得焦点
|
||||
setTimeout(() => {
|
||||
editor.foucus();
|
||||
}, 100);
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
149
public/test-drag.html
Normal file
149
public/test-drag.html
Normal file
@@ -0,0 +1,149 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>拖拽测试</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
min-height: 400px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.debug-info {
|
||||
background: #f1f5f9;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
margin: 20px 0;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 确保我们的拖拽样式生效 */
|
||||
.tiptap {
|
||||
min-height: 350px;
|
||||
padding: 20px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.tiptap .draggable-block {
|
||||
position: relative;
|
||||
cursor: grab;
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 4px;
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.tiptap .draggable-block:hover {
|
||||
background-color: rgba(59, 130, 246, 0.08);
|
||||
padding: 4px 8px;
|
||||
margin: -2px -6px;
|
||||
}
|
||||
|
||||
.tiptap .draggable-block:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.tiptap .dragging {
|
||||
opacity: 0.6;
|
||||
transform: rotate(1deg);
|
||||
background-color: rgba(59, 130, 246, 0.1) !important;
|
||||
}
|
||||
|
||||
.tiptap .ProseMirror-dropcursor {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
height: 1.2em;
|
||||
width: 3px;
|
||||
background-color: #3b82f6;
|
||||
margin-left: -1px;
|
||||
z-index: 10;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status.ready {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
border: 1px solid #bbf7d0;
|
||||
}
|
||||
|
||||
.status.dragging {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
border: 1px solid #fde68a;
|
||||
}
|
||||
|
||||
.status.dropped {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
border: 1px solid #bfdbfe;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🧪 拖拽功能测试</h1>
|
||||
|
||||
<div class="status ready" id="status">
|
||||
✅ 编辑器已就绪 - 可以开始拖拽测试
|
||||
</div>
|
||||
|
||||
<div class="debug-info" id="debug">
|
||||
等待拖拽操作...
|
||||
</div>
|
||||
|
||||
<div class="editor-container">
|
||||
<div id="editor"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 简单的调试信息显示
|
||||
function updateDebug(message) {
|
||||
document.getElementById('debug').textContent = new Date().toLocaleTimeString() + ': ' + message;
|
||||
}
|
||||
|
||||
function updateStatus(message, type = 'ready') {
|
||||
const status = document.getElementById('status');
|
||||
status.textContent = message;
|
||||
status.className = 'status ' + type;
|
||||
}
|
||||
|
||||
// 全局事件监听器来监控拖拽状态
|
||||
document.addEventListener('dragstart', () => {
|
||||
updateStatus('🟡 正在拖拽中...', 'dragging');
|
||||
updateDebug('拖拽开始');
|
||||
});
|
||||
|
||||
document.addEventListener('dragend', () => {
|
||||
updateStatus('✅ 拖拽结束', 'ready');
|
||||
updateDebug('拖拽结束');
|
||||
});
|
||||
|
||||
document.addEventListener('drop', () => {
|
||||
updateStatus('🔵 已放置', 'dropped');
|
||||
updateDebug('元素已放置');
|
||||
setTimeout(() => {
|
||||
updateStatus('✅ 编辑器已就绪 - 可以开始拖拽测试', 'ready');
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
updateDebug('页面加载完成,等待编辑器初始化...');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -2,7 +2,7 @@
|
||||
@import 'github-markdown-css/github-markdown.css';
|
||||
@import 'highlight.js/styles/github.css';
|
||||
@import './tiptap/tiptap.css';
|
||||
|
||||
@import './tiptap/styles/drag.css';
|
||||
.markdown-body,
|
||||
.tiptap {
|
||||
ul,
|
||||
|
||||
@@ -1,5 +1,65 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { basename } from '../modules/basename';
|
||||
console.log('basename', basename);
|
||||
import { TextEditor } from '../tiptap/editor';
|
||||
|
||||
export const App = () => {
|
||||
return <div className='bg-slate-200 w-full h-full border'>123</div>;
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
const editor = new TextEditor();
|
||||
|
||||
// 添加示例内容来演示拖拽功能
|
||||
const demoContent = `
|
||||
<h2>🎯 拖拽换行功能演示</h2>
|
||||
<p>欢迎使用支持拖拽的 Markdown 编辑器!这个段落可以通过拖拽来重新排序。</p>
|
||||
<p>这是第二个段落。试着将鼠标悬停在任何段落上,然后拖拽它到新的位置。</p>
|
||||
<h3>功能特点</h3>
|
||||
<ul>
|
||||
<li>支持段落拖拽重排</li>
|
||||
<li>支持标题拖拽</li>
|
||||
<li>支持列表项拖拽</li>
|
||||
<li>实时视觉反馈</li>
|
||||
</ul>
|
||||
<p>这是一个包含多行文本的较长段落。当你拖拽这个段落时,整个段落内容都会一起移动,包括所有的文本和格式。拖拽过程中会显示蓝色的指示线,帮助你精确地放置内容。</p>
|
||||
<h3>使用方法</h3>
|
||||
<p>1. 将鼠标悬停在要移动的段落上</p>
|
||||
<p>2. 按住鼠标左键开始拖拽</p>
|
||||
<p>3. 移动到目标位置时释放鼠标</p>
|
||||
<p>试试拖拽这些段落来重新组织内容吧!</p>
|
||||
`;
|
||||
|
||||
editor.createEditor(ref.current, {
|
||||
html: demoContent,
|
||||
placeholder: '开始输入内容,或拖拽现有段落来重新排序...',
|
||||
onUpdateHtml: (html) => {
|
||||
console.log('内容已更新:', html);
|
||||
}
|
||||
});
|
||||
|
||||
// 清理函数
|
||||
return () => {
|
||||
editor.destroy();
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className='bg-slate-50 w-full h-full border p-4'>
|
||||
<div className='max-w-4xl mx-auto bg-white rounded-lg shadow-sm border'>
|
||||
<div className='p-6'>
|
||||
<h1 className='text-2xl font-bold text-gray-800 mb-4'>
|
||||
Markdown 编辑器 - 拖拽功能演示
|
||||
</h1>
|
||||
<div className='bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6'>
|
||||
<p className='text-blue-800 text-sm'>
|
||||
💡 <strong>提示:</strong> 将鼠标悬停在段落上,然后拖拽它们来重新排序内容。
|
||||
</p>
|
||||
</div>
|
||||
<div ref={ref} className='min-h-96'></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,9 +3,12 @@ import StarterKit from '@tiptap/starter-kit';
|
||||
import Highlight from '@tiptap/extension-highlight';
|
||||
import Typography from '@tiptap/extension-typography';
|
||||
import { Markdown } from 'tiptap-markdown';
|
||||
import Dropcursor from '@tiptap/extension-dropcursor';
|
||||
// import Gapcursor from '@tiptap/extension-gapcursor';
|
||||
|
||||
import Placeholder from '@tiptap/extension-placeholder';
|
||||
import { Commands, getSuggestionItems, createSuggestionConfig, CommandItem } from './extensions/suggestions';
|
||||
// import { DragHandle } from './extensions/dragHandle';
|
||||
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
|
||||
import { all, createLowlight } from 'lowlight';
|
||||
// import 'highlight.js/styles/github.css';
|
||||
@@ -53,11 +56,20 @@ export class TextEditor {
|
||||
this.isInitialSetup = true;
|
||||
this.editor = new Editor({
|
||||
element: el, // 指定编辑器容器
|
||||
editable: true,
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
codeBlock: false,
|
||||
// 禁用内置的拖拽功能,使用我们的自定义拖拽
|
||||
dropcursor: false,
|
||||
}), // 使用 StarterKit 包含基础功能
|
||||
Highlight,
|
||||
Dropcursor.configure({
|
||||
color: '#3b82f6', // 设置拖拽指示器颜色
|
||||
width: 2, // 设置拖拽指示器宽度
|
||||
}),
|
||||
// Gapcursor, // 允许在难以选择的位置放置光标
|
||||
// DragHandle,
|
||||
Placeholder.configure({
|
||||
placeholder,
|
||||
}),
|
||||
@@ -119,7 +131,7 @@ export class TextEditor {
|
||||
}
|
||||
}
|
||||
setContent(html: string, emitUpdate?: boolean) {
|
||||
this.editor?.commands.setContent(html, emitUpdate);
|
||||
this.editor?.commands.setContent(html, {emitUpdate});
|
||||
}
|
||||
/**
|
||||
* before set options ,you should has element and editor
|
||||
|
||||
179
src/tiptap/extensions/dragHandle.ts
Normal file
179
src/tiptap/extensions/dragHandle.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import { Extension } from '@tiptap/core';
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state';
|
||||
import { TextSelection } from '@tiptap/pm/state';
|
||||
|
||||
function setupDraggableElements(editor: any) {
|
||||
if (editor?.view?.dom) {
|
||||
const blockElements = editor.view.dom.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, blockquote, pre');
|
||||
blockElements.forEach((element: HTMLElement) => {
|
||||
element.draggable = true;
|
||||
element.classList.add('draggable-block');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const DragHandle = Extension.create({
|
||||
name: 'dragHandle',
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey('dragHandle'),
|
||||
props: {
|
||||
handleDOMEvents: {
|
||||
dragstart: (view, event) => {
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
// 查找最近的块级元素
|
||||
let blockElement = target;
|
||||
while (blockElement && blockElement !== view.dom) {
|
||||
if (['P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'LI', 'BLOCKQUOTE', 'PRE'].includes(blockElement.nodeName)) {
|
||||
break;
|
||||
}
|
||||
blockElement = blockElement.parentElement as HTMLElement;
|
||||
}
|
||||
|
||||
if (!blockElement || blockElement === view.dom) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取块元素在文档中的位置并选中整个节点
|
||||
try {
|
||||
const pos = view.posAtDOM(blockElement, 0);
|
||||
if (pos !== null && pos >= 0) {
|
||||
const resolvedPos = view.state.doc.resolve(pos);
|
||||
|
||||
// 查找包含此位置的块级节点
|
||||
let node = resolvedPos.parent;
|
||||
let from = resolvedPos.start();
|
||||
let to = resolvedPos.end();
|
||||
|
||||
// 如果当前不是块级节点,向上查找
|
||||
let depth = resolvedPos.depth;
|
||||
while (node && !node.isBlock && depth > 0) {
|
||||
depth--;
|
||||
const parentPos = view.state.doc.resolve(resolvedPos.before(depth + 1));
|
||||
node = parentPos.parent;
|
||||
from = parentPos.start();
|
||||
to = parentPos.end();
|
||||
}
|
||||
|
||||
if (node && node.isBlock) {
|
||||
// 选中整个块级节点
|
||||
const tr = view.state.tr.setSelection(
|
||||
TextSelection.create(view.state.doc, from, to)
|
||||
);
|
||||
view.dispatch(tr);
|
||||
|
||||
console.log('Selected node:', { from, to, type: node.type.name });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error selecting node:', error);
|
||||
}
|
||||
|
||||
// 设置拖拽数据类型标识
|
||||
event.dataTransfer?.setData('application/x-tiptap-drag', 'true');
|
||||
event.dataTransfer?.setData('text/html', blockElement.outerHTML);
|
||||
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
}
|
||||
|
||||
blockElement.classList.add('dragging');
|
||||
|
||||
console.log('Drag started for:', blockElement.nodeName);
|
||||
return false;
|
||||
},
|
||||
|
||||
dragover: (view, event) => {
|
||||
event.preventDefault();
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
drop: (view, event) => {
|
||||
event.preventDefault();
|
||||
|
||||
// 检查是否是我们的拖拽
|
||||
if (!event.dataTransfer?.getData('application/x-tiptap-drag')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const coordinates = view.posAtCoords({
|
||||
left: event.clientX,
|
||||
top: event.clientY,
|
||||
});
|
||||
|
||||
if (!coordinates) {
|
||||
console.log('No coordinates found');
|
||||
return true;
|
||||
}
|
||||
|
||||
// 使用内置的拖拽处理
|
||||
const slice = view.state.selection.content();
|
||||
const { tr } = view.state;
|
||||
|
||||
// 删除原来的内容
|
||||
tr.deleteSelection();
|
||||
|
||||
// 在新位置插入
|
||||
let insertPos = coordinates.pos;
|
||||
if (insertPos > view.state.selection.from) {
|
||||
insertPos -= (view.state.selection.to - view.state.selection.from);
|
||||
}
|
||||
|
||||
tr.insert(insertPos, slice.content);
|
||||
|
||||
view.dispatch(tr);
|
||||
|
||||
console.log('Drop completed at position:', insertPos);
|
||||
return true;
|
||||
},
|
||||
|
||||
dragend: (view, event) => {
|
||||
// 清理拖拽样式
|
||||
const draggingElements = view.dom.querySelectorAll('.dragging');
|
||||
draggingElements.forEach((el) => {
|
||||
el.classList.remove('dragging');
|
||||
});
|
||||
return false;
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
|
||||
onUpdate() {
|
||||
setupDraggableElements(this.editor);
|
||||
},
|
||||
|
||||
onCreate() {
|
||||
setTimeout(() => {
|
||||
setupDraggableElements(this.editor);
|
||||
}, 100);
|
||||
|
||||
// 监听内容变化
|
||||
if (this.editor?.view?.dom) {
|
||||
const observer = new MutationObserver(() => {
|
||||
setupDraggableElements(this.editor);
|
||||
});
|
||||
|
||||
observer.observe(this.editor.view.dom, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
|
||||
(this as any)._observer = observer;
|
||||
}
|
||||
},
|
||||
|
||||
onDestroy() {
|
||||
if ((this as any)._observer) {
|
||||
(this as any)._observer.disconnect();
|
||||
}
|
||||
},
|
||||
});
|
||||
47
src/tiptap/styles/drag.css
Normal file
47
src/tiptap/styles/drag.css
Normal file
@@ -0,0 +1,47 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@@ -7,20 +7,7 @@ const version = pkgs.version || '0.0.1';
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
const basename = isDev ? '/' : pkgs?.basename || '/';
|
||||
|
||||
const checkJsh = () => {
|
||||
return process.env.SHELL === '/bin/jsh';
|
||||
};
|
||||
const isJsh = checkJsh();
|
||||
const plugins = [react(), ];
|
||||
|
||||
if (!isJsh) {
|
||||
const basicSsl = await import('@vitejs/plugin-basic-ssl');
|
||||
const tailwindcss = await import('@tailwindcss/vite');
|
||||
const defaultPlugin = basicSsl.default;
|
||||
const defaultCssPlugin = tailwindcss.default;
|
||||
plugins.push(defaultCssPlugin(),defaultPlugin());
|
||||
}
|
||||
const plugins = [react(), tailwindcss()];
|
||||
|
||||
let target = 'https://kevisual.xiongxiao.me';
|
||||
if (isDev) {
|
||||
|
||||
Reference in New Issue
Block a user