update temp

This commit is contained in:
2025-10-28 12:22:27 +08:00
parent aed46f2adc
commit 6e86ed3f65
10 changed files with 2339 additions and 783 deletions

View File

@@ -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

File diff suppressed because it is too large Load Diff

202
public/drag-demo.html Normal file
View 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
View 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>

View File

@@ -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,

View File

@@ -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>
);
};

View File

@@ -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

View 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();
}
},
});

View 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;
}
}

View File

@@ -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) {